From 2effb0ca88ed59a72b3d34e296fa88d8994fd06f Mon Sep 17 00:00:00 2001 From: Saran-nns Date: Thu, 9 Dec 2021 10:35:13 +0100 Subject: [PATCH] remove binary dist --- .gitignore | 5 + build/lib/sorn/__init__.py | 8 - build/lib/sorn/sorn.py | 1143 --------------------------- build/lib/sorn/test_sorn.py | 227 ------ build/lib/sorn/utils.py | 1163 ---------------------------- dist/sorn-0.6.2-py3-none-any.whl | Bin 21729 -> 0 bytes dist/sorn-0.6.2.tar.gz | Bin 22784 -> 0 bytes sorn.egg-info/PKG-INFO | 133 ---- sorn.egg-info/SOURCES.txt | 13 - sorn.egg-info/dependency_links.txt | 1 - sorn.egg-info/not-zip-safe | 1 - sorn.egg-info/requires.txt | 6 - sorn.egg-info/top_level.txt | 1 - 13 files changed, 5 insertions(+), 2696 deletions(-) delete mode 100644 build/lib/sorn/__init__.py delete mode 100644 build/lib/sorn/sorn.py delete mode 100644 build/lib/sorn/test_sorn.py delete mode 100644 build/lib/sorn/utils.py delete mode 100644 dist/sorn-0.6.2-py3-none-any.whl delete mode 100644 dist/sorn-0.6.2.tar.gz delete mode 100644 sorn.egg-info/PKG-INFO delete mode 100644 sorn.egg-info/SOURCES.txt delete mode 100644 sorn.egg-info/dependency_links.txt delete mode 100644 sorn.egg-info/not-zip-safe delete mode 100644 sorn.egg-info/requires.txt delete mode 100644 sorn.egg-info/top_level.txt diff --git a/.gitignore b/.gitignore index 888f5bb..86d3ad3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,11 @@ __pycache__/ # Notebook checkpoints .ipynb_checkpoints +# Pypi binaries +build/ +dist/ +sorn.egg-info/ + ### VisualStudioCode ### .vscode/* !.vscode/tasks.json diff --git a/build/lib/sorn/__init__.py b/build/lib/sorn/__init__.py deleted file mode 100644 index 98fd132..0000000 --- a/build/lib/sorn/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .sorn import Simulator, Trainer -import logging -from .utils import * - -__author__ = "Saranraj Nambusubramaniyan" -__version__ = "0.6.2" - -logging.basicConfig(level=logging.INFO) diff --git a/build/lib/sorn/sorn.py b/build/lib/sorn/sorn.py deleted file mode 100644 index c23eafc..0000000 --- a/build/lib/sorn/sorn.py +++ /dev/null @@ -1,1143 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import division -import numpy as np -import os -import random - -try: - from sorn.utils import Initializer -except: - from utils import Initializer - -class Sorn(object): - - """ This class wraps initialization of the network and its parameters""" - - nu = 10 - ne = 200 - ni = int(0.2 * ne) - eta_stdp = 0.004 - eta_inhib = 0.001 - eta_ip = 0.01 - te_max = 1.0 - ti_max = 0.5 - ti_min = 0.0 - te_min = 0.0 - mu_ip = 0.1 - sigma_ip = 0.0 - network_type_ee = "Sparse" - network_type_ei = "Sparse" - network_type_ie = "Dense" - lambda_ee = 20 - lambda_ei = 40 - lambda_ie = 100 - - @staticmethod - def initialize_weight_matrix(network_type: str, synaptic_connection: str, self_connection: str, lambd_w: int - ): - """Wrapper for initializing the weight matrices for SORN - - Args: - network_type (str): Spare or Dense - - synaptic_connection (str): EE,EI,IE. Note that Spare connection is defined only for EE connections - - self_connection (str): True or False: Synaptic delay or time delay - - lambd_w (int): Average number of incoming and outgoing connections per neuron - - Returns: - weight_matrix (array): Array of connection strengths - """ - - if (network_type == "Sparse") and (self_connection == "False"): - - # Generate weight matrix for E-E/ E-I connections with mean lamda incoming and out-going connections per neuron - assert (lambd_w <= Sorn.ne), "Number of connections per unit (lambda) should be less than number of units(Ne) in the pool and also Ne should be greater than 25" - weight_matrix = Initializer.generate_lambd_connections( - synaptic_connection, Sorn.ne, Sorn.ni, lambd_w, lambd_std=1 - ) - - # Dense matrix for W_ie - elif (network_type == "Dense") and (self_connection == "False"): - - # Uniform distribution of weights - weight_matrix = np.random.uniform(0.0, 0.1, (Sorn.ne, Sorn.ni)) - weight_matrix.reshape((Sorn.ne, Sorn.ni)) - - return weight_matrix - - @staticmethod - def initialize_threshold_matrix(te_min: float, te_max: float, ti_min: float, ti_max: float): - """Initialize the threshold for excitatory and inhibitory neurons - - Args: - te_min (float): Min threshold value for excitatory units - te_max (float): Min threshold value for inhibitory units - ti_min (float): Max threshold value for excitatory units - ti_max (float): Max threshold value for inhibitory units - - Returns: - te (array): Threshold values for excitatory units - ti (array): Threshold values for inhibitory units - """ - - te = np.random.uniform(te_min, te_max, (Sorn.ne, 1)) - ti = np.random.uniform(ti_min, ti_max, (Sorn.ni, 1)) - - return te, ti - - @staticmethod - def initialize_activity_vector(ne: int, ni: int): - """Initialize the activity vectors X and Y for excitatory and inhibitory neurons - - Args: - ne (int): Number of excitatory neurons - ni (int): Number of inhibitory neurons - - Returns: - x (array): Array of activity vectors of excitatory population - y (array): Array of activity vectors of inhibitory population""" - - x = np.zeros((ne, 2)) - y = np.zeros((ni, 2)) - - return x, y - -class Plasticity(Sorn): - """Instance of class Sorn. Inherits the variables and functions defined in class Sorn. - It encapsulates all plasticity mechanisms mentioned in the article. Inherits all attributed from parent class Sorn - """ - - def __init__(self): - - super().__init__() - self.nu = Sorn.nu # Number of input units - self.ne = Sorn.ne # Number of excitatory units - self.eta_stdp = ( - Sorn.eta_stdp - ) # STDP plasticity Learning rate constant; SORN1 and SORN2 - self.eta_ip = ( - Sorn.eta_ip - ) # Intrinsic plasticity learning rate constant; SORN1 and SORN2 - self.eta_inhib = ( - Sorn.eta_inhib - ) # Intrinsic plasticity learning rate constant; SORN2 only - self.h_ip = 2 * Sorn.nu / Sorn.ne # Target firing rate - self.mu_ip = Sorn.mu_ip # Mean target firing rate - # Number of inhibitory units in the network - self.ni = int(0.2 * Sorn.ne) - self.time_steps = Sorn.time_steps # Total time steps of simulation - self.te_min = Sorn.te_min # Excitatory minimum Threshold - self.te_max = Sorn.te_max # Excitatory maximum Threshold - - def stdp(self, wee: np.array, x: np.array, cutoff_weights: list): - """Apply STDP rule : Regulates synaptic strength between the pre(Xj) and post(Xi) synaptic neurons - - Args: - wee (array): Weight matrix - - x (array): Excitatory network activity - - cutoff_weights (list): Maximum and minimum weight ranges - - Returns: - wee (array): Weight matrix - """ - - x = np.asarray(x) - xt_1 = x[:, 0] - xt = x[:, 1] - wee_t = wee.copy() - - # STDP applies only on the neurons which are connected. - - for i in range(len(wee_t[0])): # Each neuron i, Post-synaptic neuron - - for j in range( - len(wee_t[0:]) - ): # Incoming connection from jth pre-synaptic neuron to ith neuron - - if wee_t[j][i] != 0.0: # Check connectivity - - # Get the change in weight - delta_wee_t = self.eta_stdp * \ - (xt[i] * xt_1[j] - xt_1[i] * xt[j]) - - # Update the weight between jth neuron to i ""Different from notation in article - - wee_t[j][i] = wee[j][i] + delta_wee_t - - # Prune the smallest weights induced by plasticity mechanisms; Apply lower cutoff weight - wee_t = Initializer.prune_small_weights(wee_t, cutoff_weights[0]) - - # Check and set all weights < upper cutoff weight - wee_t = Initializer.set_max_cutoff_weight(wee_t, cutoff_weights[1]) - - return wee_t - - def ip(self, te: np.array, x: np.array): - """Intrinsic Plasiticity mechanism - - Args: - te (array): Threshold vector of excitatory units - - x (array): Excitatory network activity - - Returns: - te (array): Threshold vector of excitatory units - """ - - # IP rule: Active unit increases its threshold and inactive decreases its threshold. - xt = x[:, 1] - - te_update = te + self.eta_ip * (xt.reshape(self.ne, 1) - self.h_ip) - - # Check whether all te are in range [0.0,1.0] and update acordingly - - # Update te < 0.0 ---> 0.0 - # te_update = prune_small_weights(te_update,self.te_min) - - # Set all te > 1.0 --> 1.0 - # te_update = set_max_cutoff_weight(te_update,self.te_max) - - return te_update - - @staticmethod - def ss(wee: np.array): - """Synaptic Scaling or Synaptic Normalization - - Args: - wee (array): Weight matrix - - Returns: - wee (array): Scaled Weight matrix - """ - wee = wee / np.sum(wee, axis=0) - return wee - - def istdp(self, wei: np.array, x: np.array, y: np.array, cutoff_weights: list): - """Apply iSTDP rule, which regulates synaptic strength between the pre inhibitory(Xj) and post Excitatory(Xi) synaptic neurons - - Args: - wei (array): Synaptic strengths from inhibitory to excitatory - - x (array): Excitatory network activity - - y (array): Inhibitory network activity - - cutoff_weights (list): Maximum and minimum weight ranges - - Returns: - wei (array): Synaptic strengths from inhibitory to excitatory""" - - # Excitatory network activity - xt = np.asarray(x)[:, 1] - - # Inhibitory network activity - yt_1 = np.asarray(y)[:, 0] - - # iSTDP applies only on the neurons which are connected. - wei_t = wei.copy() - - for i in range( - len(wei_t[0]) - ): # Each neuron i, Post-synaptic neuron: means for each column; - - for j in range( - len(wei_t[0:]) - ): # Incoming connection from j, pre-synaptic neuron to ith neuron - - if wei_t[j][i] != 0.0: # Check connectivity - - # Get the change in weight - delta_wei_t = ( - -self.eta_inhib * yt_1[j] * - (1 - xt[i] * (1 + 1 / self.mu_ip)) - ) - - # Update the weight between jth neuron to i ""Different from notation in article - - wei_t[j][i] = wei[j][i] + delta_wei_t - - # Prune the smallest weights induced by plasticity mechanisms; Apply lower cutoff weight - wei_t = Initializer.prune_small_weights(wei_t, cutoff_weights[0]) - - # Check and set all weights < upper cutoff weight - wei_t = Initializer.set_max_cutoff_weight(wei_t, cutoff_weights[1]) - - return wei_t - - @staticmethod - def structural_plasticity(wee: np.array): - """Add new connection value to the smallest weight between excitatory units randomly - - Args: - wee (array): Weight matrix - - Returns: - wee (array): Weight matrix""" - - p_c = np.random.randint(0, 10, 1) - - if p_c == 0: # p_c= 0.1 - - # Do structural plasticity - # Choose the smallest weights randomly from the weight matrix wee - indexes = Initializer.get_unconnected_indexes(wee) - - # Choose any idx randomly such that i!=j - while True: - idx_rand = random.choice(indexes) - if idx_rand[0] != idx_rand[1]: - break - - wee[idx_rand[0]][idx_rand[1]] = 0.001 - - return wee - - @staticmethod - def initialize_plasticity(): - """Initialize weight matrices for plasticity phase based on network configuration - - Args: - kwargs (self.__dict__): All arguments are inherited from Sorn attributes - - Returns: - tuple(array): Weight matrices WEI, WEE, WIE and threshold matrices Te, Ti and Initial state vectors X,Y """ - - sorn_init = Sorn() - WEE_init = sorn_init.initialize_weight_matrix( - network_type=Sorn.network_type_ee, - synaptic_connection="EE", - self_connection="False", - lambd_w=Sorn.lambda_ee, - ) - WEI_init = sorn_init.initialize_weight_matrix( - network_type=Sorn.network_type_ei, - synaptic_connection="EI", - self_connection="False", - lambd_w=Sorn.lambda_ei, - ) - WIE_init = sorn_init.initialize_weight_matrix( - network_type=Sorn.network_type_ie, - synaptic_connection="IE", - self_connection="False", - lambd_w=Sorn.lambda_ie, - ) - - Wee_init = Initializer.zero_sum_incoming_check(WEE_init) - # Wei_init = initializer.zero_sum_incoming_check(WEI_init.T) # For SORN 1 - Wei_init = Initializer.zero_sum_incoming_check(WEI_init) - Wie_init = Initializer.zero_sum_incoming_check(WIE_init) - - c = np.count_nonzero(Wee_init) - v = np.count_nonzero(Wei_init) - b = np.count_nonzero(Wie_init) - - print("Network Initialized") - print("Number of connections in Wee %s , Wei %s, Wie %s" % (c, v, b)) - print( - "Shapes Wee %s Wei %s Wie %s" - % (Wee_init.shape, Wei_init.shape, Wie_init.shape) - ) - - # Normalize the incoming weights - - normalized_wee = Initializer.normalize_weight_matrix(Wee_init) - normalized_wei = Initializer.normalize_weight_matrix(Wei_init) - normalized_wie = Initializer.normalize_weight_matrix(Wie_init) - - te_init, ti_init = sorn_init.initialize_threshold_matrix( - Sorn.te_min, Sorn.te_max, Sorn.ti_min, Sorn.ti_max - ) - x_init, y_init = sorn_init.initialize_activity_vector( - Sorn.ne, Sorn.ni) - - # Initializing variables from sorn_initialize.py - - wee = normalized_wee.copy() - wei = normalized_wei.copy() - wie = normalized_wie.copy() - te = te_init.copy() - ti = ti_init.copy() - x = x_init.copy() - y = y_init.copy() - - return wee, wei, wie, te, ti, x, y - - -class MatrixCollection(Sorn): - """Collect all matrices initialized and updated during simulation (plasiticity and training phases) - - Args: - phase (str): Training or Plasticity phase - - matrices (dict): Network activity, threshold and connection matrices - - Returns: - MatrixCollection instance""" - - def __init__(self, phase: str, matrices: dict = None): - super().__init__() - - self.phase = phase - self.matrices = matrices - if self.phase == "plasticity" and self.matrices == None: - - self.time_steps = Sorn.time_steps + 1 # Total training steps - self.Wee, self.Wei, self.Wie, self.Te, self.Ti, self.X, self.Y = ( - [0] * self.time_steps, - [0] * self.time_steps, - [0] * self.time_steps, - [0] * self.time_steps, - [0] * self.time_steps, - [0] * self.time_steps, - [0] * self.time_steps, - ) - wee, wei, wie, te, ti, x, y = Plasticity.initialize_plasticity() - - # Assign initial matrix to the master matrices - self.Wee[0] = wee - self.Wei[0] = wei - self.Wie[0] = wie - self.Te[0] = te - self.Ti[0] = ti - self.X[0] = x - self.Y[0] = y - - elif self.phase == "plasticity" and self.matrices != None: - - self.time_steps = Sorn.time_steps + 1 # Total training steps - self.Wee, self.Wei, self.Wie, self.Te, self.Ti, self.X, self.Y = ( - [0] * self.time_steps, - [0] * self.time_steps, - [0] * self.time_steps, - [0] * self.time_steps, - [0] * self.time_steps, - [0] * self.time_steps, - [0] * self.time_steps, - ) - # Assign matrices from plasticity phase to the new master matrices for training phase - self.Wee[0] = matrices["Wee"] - self.Wei[0] = matrices["Wei"] - self.Wie[0] = matrices["Wie"] - self.Te[0] = matrices["Te"] - self.Ti[0] = matrices["Ti"] - self.X[0] = matrices["X"] - self.Y[0] = matrices["Y"] - - elif self.phase == "training": - - # NOTE:time_steps here is diferent for plasticity and training phase - self.time_steps = Sorn.time_steps + 1 # Total training steps - self.Wee, self.Wei, self.Wie, self.Te, self.Ti, self.X, self.Y = ( - [0] * self.time_steps, - [0] * self.time_steps, - [0] * self.time_steps, - [0] * self.time_steps, - [0] * self.time_steps, - [0] * self.time_steps, - [0] * self.time_steps, - ) - # Assign matrices from plasticity phase to new respective matrices for training phase - self.Wee[0] = matrices["Wee"] - self.Wei[0] = matrices["Wei"] - self.Wie[0] = matrices["Wie"] - self.Te[0] = matrices["Te"] - self.Ti[0] = matrices["Ti"] - self.X[0] = matrices["X"] - self.Y[0] = matrices["Y"] - - def weight_matrix(self, wee: np.array, wei: np.array, wie: np.array, i: int): - """Update weight matrices - - Args: - wee (array): Excitatory-Excitatory weight matrix - - wei (array): Inhibitory-Excitatory weight matrix - - wie (array): Excitatory-Inhibitory weight matrix - - i (int): Time step - Returns: - tuple (array): Weight Matrices Wee, Wei, Wie""" - - self.Wee[i + 1] = wee - self.Wei[i + 1] = wei - self.Wie[i + 1] = wie - - return self.Wee, self.Wei, self.Wie - - def threshold_matrix(self, te: np.array, ti: np.array, i: int): - """Update threshold matrices - - Args: - te (array): Excitatory threshold - - ti (array): Inhibitory threshold - - i (int): Time step - - Returns: - tuple (array): Threshold Matrices Te and Ti""" - - self.Te[i + 1] = te - self.Ti[i + 1] = ti - return self.Te, self.Ti - - def network_activity_t( - self, excitatory_net: np.array, inhibitory_net: np.array, i: int - ): - """Network state at current time step - - Args: - excitatory_net (array): Excitatory network activity - - inhibitory_net (array): Inhibitory network activity - - i (int): Time step - - Returns: - tuple (array): Updated Excitatory and Inhibitory states - """ - - self.X[i + 1] = excitatory_net - self.Y[i + 1] = inhibitory_net - - return self.X, self.Y - - def network_activity_t_1(self, x: np.array, y: np.array, i: int): - """Network activity at previous time step - - Args: - x (array): Excitatory network activity - - y (array): Inhibitory network activity - - i (int): Time step - - Returns: - tuple (array): Previous Excitatory and Inhibitory states - """ - x_1, y_1 = [0] * self.time_steps, [0] * self.time_steps - x_1[i] = x - y_1[i] = y - - return x_1, y_1 - - -class NetworkState(Plasticity): - - """The evolution of network states - - Args: - v_t (array): External input/stimuli - - Returns: - instance (object): NetworkState instance""" - - def __init__(self, v_t: np.array): - super().__init__() - self.v_t = v_t - assert Sorn.nu == len( - self.v_t - ), "Input units and input size mismatch: {} != {}".format( - Sorn.nu, len(self.v_t) - ) - if Sorn.nu != Sorn.ne: - self.v_t = list(self.v_t) + [0.0] * (Sorn.ne - Sorn.nu) - self.v_t = np.expand_dims(self.v_t, 1) - - def incoming_drive(self, weights: np.array, activity_vector: np.array): - """Excitatory Post synaptic potential towards neurons in the reservoir in the absence of external input - - Args: - weights (array): Synaptic strengths - - activity_vector (list): Acitivity of inhibitory or Excitatory neurons - - Returns: - incoming (array): Excitatory Post synaptic potential towards neurons - """ - incoming = weights * activity_vector - incoming = np.array(incoming.sum(axis=0)) - return incoming - - def excitatory_network_state( - self, - wee: np.array, - wei: np.array, - te: np.array, - x: np.array, - y: np.array, - white_noise_e: np.array, - ): - """Activity of Excitatory neurons in the network - - Args: - wee (array): Excitatory-Excitatory weight matrix - - wei (array): Inhibitory-Excitatory weight matrix - - te (array): Excitatory threshold - - x (array): Excitatory network activity - - y (array): Inhibitory network activity - - white_noise_e (array): Gaussian noise - - Returns: - x (array): Current Excitatory network activity - """ - xt = x[:, 1] - xt = xt.reshape(self.ne, 1) - yt = y[:, 1] - yt = yt.reshape(self.ni, 1) - - incoming_drive_e = np.expand_dims( - self.incoming_drive(weights=wee, activity_vector=xt), 1 - ) - incoming_drive_i = np.expand_dims( - self.incoming_drive(weights=wei, activity_vector=yt), 1 - ) - tot_incoming_drive = ( - incoming_drive_e - - incoming_drive_i - + white_noise_e - + np.asarray(self.v_t) - - te - ) - - # Heaviside step function - heaviside_step = np.expand_dims([0.0] * len(tot_incoming_drive), 1) - heaviside_step[tot_incoming_drive > 0] = 1.0 - return heaviside_step - - def inhibitory_network_state( - self, wie: np.array, ti: np.array, y: np.array, white_noise_i: np.array - ): - """Activity of Excitatory neurons in the network - - Args: - wee (array): Excitatory-Excitatory weight matrix - - wie (array): Excitatory-Inhibitory weight matrix - - ti (array): Inhibitory threshold - - y (array): Inhibitory network activity - - white_noise_i (array): Gaussian noise - - Returns: - y (array): Current Inhibitory network activity""" - - wie = np.asarray(wie) - yt = y[:, 1] - yt = yt.reshape(Sorn.ne, 1) - - incoming_drive_e = np.expand_dims( - self.incoming_drive(weights=wie, activity_vector=yt), 1 - ) - - tot_incoming_drive = incoming_drive_e + white_noise_i - ti - heaviside_step = np.expand_dims([0.0] * len(tot_incoming_drive), 1) - heaviside_step[tot_incoming_drive > 0] = 1.0 - - return heaviside_step - - def recurrent_drive( - self, - wee: np.array, - wei: np.array, - te: np.array, - x: np.array, - y: np.array, - white_noise_e: np.array, - ): - """Network state due to recurrent drive received by the each unit at time t+1. Activity of Excitatory neurons without external stimuli - - Args: - - wee (array): Excitatory-Excitatory weight matrix - - wei (array): Inhibitory-Excitatory weight matrix - - te (array): Excitatory threshold - - x (array): Excitatory network activity - - y (array): Inhibitory network activity - - white_noise_e (array): Gaussian noise - - Returns: - xt (array): Recurrent network state - """ - xt = x[:, 1] - xt = xt.reshape(self.ne, 1) - yt = y[:, 1] - yt = yt.reshape(self.ni, 1) - - incoming_drive_e = np.expand_dims( - self.incoming_drive(weights=wee, activity_vector=xt), 1 - ) - incoming_drive_i = np.expand_dims( - self.incoming_drive(weights=wei, activity_vector=yt), 1 - ) - - tot_incoming_drive = incoming_drive_e - incoming_drive_i + white_noise_e - te - - heaviside_step = np.expand_dims([0.0] * len(tot_incoming_drive), 1) - heaviside_step[tot_incoming_drive > 0] = 1.0 - - return heaviside_step - - -# Simulate / Train SORN -class Simulator_(Sorn): - - """Simulate SORN using external input/noise using the fresh or pretrained matrices - - Args: - inputs (np.array, optional): External stimuli. Defaults to None. - - phase (str, optional): Plasticity phase. Defaults to "plasticity". - - matrices (dict, optional): Network states, connections and threshold matrices. Defaults to None. - - time_steps (int, optional): Total number of time steps to simulate the network. Defaults to 1. - - noise (bool, optional): If True, noise will be added. Defaults to True. - - Returns: - plastic_matrices (dict): Network states, connections and threshold matrices - - X_all (array): Excitatory network activity collected during entire simulation steps - - Y_all (array): Inhibitory network activity collected during entire simulation steps - - R_all (array): Recurrent network activity collected during entire simulation steps - - frac_pos_active_conn (list): Number of positive connection strengths in the network at each time step during simulation""" - - def __init__(self): - super().__init__() - pass - - def simulate_sorn( - self, - inputs: np.array = None, - phase: str = "plasticity", - matrices: dict = None, - time_steps: int = None, - noise: bool = True, - freeze: list = None, - **kwargs - ): - """Simulation/Plasticity phase - - Args: - inputs (np.array, optional): External stimuli. Defaults to None. - - phase (str, optional): Plasticity phase. Defaults to "plasticity" - - matrices (dict, optional): Network states, connections and threshold matrices. Defaults to None. - - time_steps (int, optional): Total number of time steps to simulate the network. Defaults to 1. - - noise (bool, optional): If True, noise will be added. Defaults to True. - - freeze (list, optional): List of synaptic plasticity mechanisms which will be turned off during simulation. Defaults to None. - - Returns: - plastic_matrices (dict): Network states, connections and threshold matrices - - X_all (array): Excitatory network activity collected during entire simulation steps - - Y_all (array): Inhibitory network activity collected during entire simulation steps - - R_all (array): Recurrent network activity collected during entire simulation steps - - frac_pos_active_conn (list): Number of positive connection strengths in the network at each time step during simulation""" - - assert ( - phase == "plasticity" or "training" - ), "Phase can be either 'plasticity' or 'training'" - - self.time_steps = time_steps - Sorn.time_steps = time_steps - self.phase = phase - self.matrices = matrices - self.freeze = [] if freeze == None else freeze - - kwargs_ = [ - "ne", - "nu", - "network_type_ee", - "network_type_ei", - "network_type_ie", - "lambda_ee", - "lambda_ei", - "lambda_ie", - "eta_stdp", - "eta_inhib", - "eta_ip", - "te_max", - "ti_max", - "ti_min", - "te_min", - "mu_ip", - "sigma_ip", - ] - for key, value in kwargs.items(): - if key in kwargs_: - setattr(Sorn, key, value) - # assert Sorn.nu == len(inputs[:,0]),"Size mismatch: Input != Nu " - Sorn.ni = int(0.2 * Sorn.ne) - - # Initialize/Get the weight, threshold matrices and activity vectors - matrix_collection = MatrixCollection( - phase=self.phase, matrices=self.matrices) - - # Collect the network activity at all time steps - - X_all = [0] * self.time_steps - Y_all = [0] * self.time_steps - R_all = [0] * self.time_steps - - frac_pos_active_conn = [] - - # To get the last activation status of Exc and Inh neurons - for i in range(self.time_steps): - - if noise: - white_noise_e = Initializer.white_gaussian_noise( - mu=0.0, sigma=0.04, t=Sorn.ne - ) - white_noise_i = Initializer.white_gaussian_noise( - mu=0.0, sigma=0.04, t=Sorn.ni - ) - else: - white_noise_e, white_noise_i = 0.0, 0.0 - - network_state = NetworkState( - inputs[:, i] - ) - - # Buffers to get the resulting x and y vectors at the current time step and update the master matrix - x_buffer, y_buffer = np.zeros( - (Sorn.ne, 2)), np.zeros((Sorn.ni, 2)) - - te_buffer, ti_buffer = np.zeros( - (Sorn.ne, 1)), np.zeros((Sorn.ni, 1)) - - Wee, Wei, Wie = ( - matrix_collection.Wee, - matrix_collection.Wei, - matrix_collection.Wie, - ) - Te, Ti = matrix_collection.Te, matrix_collection.Ti - X, Y = matrix_collection.X, matrix_collection.Y - - # Fraction of active connections between E-E network - frac_pos_active_conn.append((Wee[i] > 0.0).sum()) - - # Recurrent drive - r = network_state.recurrent_drive( - Wee[i], Wei[i], Te[i], X[i], Y[i], white_noise_e - ) - - # Get excitatory states and inhibitory states given the weights and thresholds - # x(t+1), y(t+1) - excitatory_state_xt_buffer = network_state.excitatory_network_state( - Wee[i], Wei[i], Te[i], X[i], Y[i], white_noise_e - ) - inhibitory_state_yt_buffer = network_state.inhibitory_network_state( - Wie[i], Ti[i], X[i], white_noise_i - ) - - # Update X and Y - x_buffer[:, 0] = X[i][:, 1] # xt -->(becomes) xt_1 - x_buffer[ - :, 1 - ] = excitatory_state_xt_buffer.T # New_activation; x_buffer --> xt - - y_buffer[:, 0] = Y[i][:, 1] - y_buffer[:, 1] = inhibitory_state_yt_buffer.T - - # Plasticity phase - plasticity = Plasticity() - - # STDP - if 'stdp' not in self.freeze: - Wee[i] = plasticity.stdp( - Wee[i], x_buffer, cutoff_weights=(0.0, 1.0)) - - # Intrinsic plasticity - if 'ip' not in self.freeze: - Te[i] = plasticity.ip(Te[i], x_buffer) - - # Structural plasticity - if 'sp' not in self.freeze: - Wee[i] = plasticity.structural_plasticity(Wee[i]) - - # iSTDP - if 'istdp' not in self.freeze: - Wei[i] = plasticity.istdp( - Wei[i], x_buffer, y_buffer, cutoff_weights=(0.0, 1.0) - ) - - # Synaptic scaling Wee - if 'ss' not in self.freeze: - Wee[i] = plasticity.ss(Wee[i]) - Wei[i] = plasticity.ss(Wei[i]) - - # Assign the matrices to the matrix collections - matrix_collection.weight_matrix(Wee[i], Wei[i], Wie[i], i) - matrix_collection.threshold_matrix(Te[i], Ti[i], i) - matrix_collection.network_activity_t(x_buffer, y_buffer, i) - - X_all[i] = x_buffer[:, 1] - Y_all[i] = y_buffer[:, 1] - R_all[i] = r - - plastic_matrices = { - "Wee": matrix_collection.Wee[-1], - "Wei": matrix_collection.Wei[-1], - "Wie": matrix_collection.Wie[-1], - "Te": matrix_collection.Te[-1], - "Ti": matrix_collection.Ti[-1], - "X": X[-1], - "Y": Y[-1], - } - - return plastic_matrices, X_all, Y_all, R_all, frac_pos_active_conn - - -class Trainer_(Sorn): - """Train the network with the fresh or pretrained network matrices and external stimuli - - Args: - inputs (np.array, optional): External stimuli. Defaults to None. - - phase (str, optional): Training phase. Defaults to "training". - - matrices (dict, optional): Network states, connections and threshold matrices. Defaults to None. - - time_steps (int, optional): Total number of time steps to simulate the network. Defaults to 1. - - noise (bool, optional): If True, noise will be added. Defaults to True. - - freeze (list, optional): List of synaptic plasticity mechanisms which will be turned off during simulation. Defaults to None. - - Returns: - plastic_matrices (dict): Network states, connections and threshold matrices - - X_all (array): Excitatory network activity collected during entire simulation steps - - Y_all (array): Inhibitory network activity collected during entire simulation steps - - R_all (array): Recurrent network activity collected during entire simulation steps - - frac_pos_active_conn (list): Number of positive connection strengths in the network at each time step during simulation""" - - def __init__(self): - super().__init__() - pass - - def train_sorn( - self, - inputs: np.array = None, - phase: str = "training", - matrices: dict = None, - time_steps: int = None, - noise: bool = True, - freeze: list = None, - **kwargs - ): - """Train the network with the fresh or pretrained network matrices and external stimuli - - Args: - inputs (np.array, optional): External stimuli. Defaults to None. - - phase (str, optional): Training phase. Defaults to "training". - - matrices (dict, optional): Network states, connections and threshold matrices. Defaults to None. - - time_steps (int, optional): Total number of time steps to simulate the network. Defaults to 1. - - noise (bool, optional): If True, noise will be added. Defaults to True. - - Returns: - - plastic_matrices (dict): Network states, connections and threshold matrices - - X_all (array): Excitatory network activity collected during entire simulation steps - - Y_all (array): Inhibitory network activity collected during entire simulation steps - - R_all (array): Recurrent network activity collected during entire simulation steps - - frac_pos_active_conn (list): Number of positive connection strengths in the network at each time step during simulation""" - - assert ( - phase == "plasticity" or "training" - ), "Phase can be either 'plasticity' or 'training'" - - kwargs_ = [ - "ne", - "nu", - "network_type_ee", - "network_type_ei", - "network_type_ie", - "lambda_ee", - "lambda_ei", - "lambda_ie", - "eta_stdp", - "eta_inhib", - "eta_ip", - "te_max", - "ti_max", - "ti_min", - "te_min", - "mu_ip", - "sigma_ip", - ] - for key, value in kwargs.items(): - if key in kwargs_: - setattr(Sorn, key, value) - Sorn.ni = int(0.2 * Sorn.ne) - # assert Sorn.nu == len(inputs[:,0]),"Size mismatch: Input != Nu " - - self.phase = phase - self.matrices = matrices - self.time_steps = time_steps - Sorn.time_steps = time_steps - self.inputs = np.asarray(inputs) - self.freeze = [] if freeze == None else freeze - - X_all = [0] * self.time_steps - Y_all = [0] * self.time_steps - R_all = [0] * self.time_steps - - frac_pos_active_conn = [] - - matrix_collection = MatrixCollection( - phase=self.phase, matrices=self.matrices) - - for i in range(self.time_steps): - - if noise: - white_noise_e = Initializer.white_gaussian_noise( - mu=0.0, sigma=0.04, t=Sorn.ne - ) - white_noise_i = Initializer.white_gaussian_noise( - mu=0.0, sigma=0.04, t=Sorn.ni - ) - else: - white_noise_e = 0.0 - white_noise_i = 0.0 - - network_state = NetworkState( - self.inputs[:, i] - ) - - # Buffers to get the resulting x and y vectors at the current time step and update the master matrix - x_buffer, y_buffer = np.zeros( - (Sorn.ne, 2)), np.zeros((Sorn.ni, 2)) - te_buffer, ti_buffer = np.zeros( - (Sorn.ne, 1)), np.zeros((Sorn.ni, 1)) - - Wee, Wei, Wie = ( - matrix_collection.Wee, - matrix_collection.Wei, - matrix_collection.Wie, - ) - Te, Ti = matrix_collection.Te, matrix_collection.Ti - X, Y = matrix_collection.X, matrix_collection.Y - - # Fraction of active connections between E-E network - frac_pos_active_conn.append((Wee[i] > 0.0).sum()) - - # Recurrent drive at t+1 used to predict the next external stimuli - r = network_state.recurrent_drive( - Wee[i], Wei[i], Te[i], X[i], Y[i], white_noise_e=white_noise_e - ) - - # Get excitatory states and inhibitory states given the weights and thresholds - # x(t+1), y(t+1) - excitatory_state_xt_buffer = network_state.excitatory_network_state( - Wee[i], Wei[i], Te[i], X[i], Y[i], white_noise_e=white_noise_e - ) - inhibitory_state_yt_buffer = network_state.inhibitory_network_state( - Wie[i], Ti[i], X[i], white_noise_i=white_noise_i - ) - - # Update X and Y - x_buffer[:, 0] = X[i][:, 1] # xt -->xt_1 - x_buffer[:, 1] = excitatory_state_xt_buffer.T # x_buffer --> xt - y_buffer[:, 0] = Y[i][:, 1] - y_buffer[:, 1] = inhibitory_state_yt_buffer.T - - if self.phase == "plasticity": - plasticity = Plasticity() - - # STDP - if 'stdp' not in self.freeze: - Wee[i] = plasticity.stdp( - Wee[i], x_buffer, cutoff_weights=(0.0, 1.0)) - - # Intrinsic plasticity - if 'ip' not in self.freeze: - Te[i] = plasticity.ip(Te[i], x_buffer) - - # Structural plasticity - if 'sp' not in self.freeze: - Wee[i] = plasticity.structural_plasticity(Wee[i]) - - # iSTDP - if 'istdp' not in self.freeze: - Wei[i] = plasticity.istdp( - Wei[i], x_buffer, y_buffer, cutoff_weights=(0.0, 1.0) - ) - - # Synaptic scaling Wee - if 'ss' not in self.freeze: - Wee[i] = plasticity.ss(Wee[i]) - - # Synaptic scaling Wei - Wei[i] = plasticity.ss(Wei[i]) - else: - # Wee[i], Wei[i], Te[i] remain same - pass - - # Assign the matrices to the matrix collections - matrix_collection.weight_matrix(Wee[i], Wei[i], Wie[i], i) - matrix_collection.threshold_matrix(Te[i], Ti[i], i) - matrix_collection.network_activity_t(x_buffer, y_buffer, i) - - X_all[i] = x_buffer[:, 1] - Y_all[i] = y_buffer[:, 1] - R_all[i] = r - - plastic_matrices = { - "Wee": matrix_collection.Wee[-1], - "Wei": matrix_collection.Wei[-1], - "Wie": matrix_collection.Wie[-1], - "Te": matrix_collection.Te[-1], - "Ti": matrix_collection.Ti[-1], - "X": X[-1], - "Y": Y[-1], - } - - return plastic_matrices, X_all, Y_all, R_all, frac_pos_active_conn - - -Trainer = Trainer_() -Simulator = Simulator_() -if __name__ == "__main__": - pass \ No newline at end of file diff --git a/build/lib/sorn/test_sorn.py b/build/lib/sorn/test_sorn.py deleted file mode 100644 index 01a0c38..0000000 --- a/build/lib/sorn/test_sorn.py +++ /dev/null @@ -1,227 +0,0 @@ -import unittest -import pickle -import numpy as np -from sorn.sorn import RunSorn, Generator -from sorn.utils import Plotter, Statistics, Initializer -from sorn import Simulator, Trainer - -num_features = 10 -inputs = np.random.rand(num_features, 1) - -# Get the pickled matrices: -with open("sample_matrices.pkl", "rb") as f: - ( - matrices_dict, - Exc_activity, - Inh_activity, - Rec_activity, - num_active_connections, - ) = pickle.load(f) - - -class TestSorn(unittest.TestCase): - def test_runsorn(self): - - self.assertRaises( - Exception, Generator().get_initial_matrices("./sorn/")) - - matrices_dict = Generator().get_initial_matrices("./sorn") - - self.assertRaises( - Exception, RunSorn(phase="Plasticity", - matrices=None).run_sorn([0.0]) - ) - - self.assertRaises( - Exception, RunSorn( - phase="Training", matrices=matrices_dict).run_sorn([0.0]) - ) - - self.assertRaises( - Exception, - Simulator.simulate_sorn( - inputs=[0.0], - phase="plasticity", - matrices=None, - noise=True, - time_steps=2, - ne=20, - nu=1, - ), - ) - - self.assertRaises( - Exception, - Simulator.simulate_sorn( - inputs=[0.0], - phase="plasticity", - matrices=matrices_dict, - noise=True, - time_steps=2, - ne=20, - nu=10, - ), - ) - - self.assertRaises( - Exception, - Trainer.train_sorn( - inputs=inputs, - phase="Training", - matrices=matrices_dict, - nu=num_features, - time_steps=1, - ), - ) - - def test_plotter(self): - - self.assertRaises( - Exception, - Plotter.hist_outgoing_conn( - weights=matrices_dict["Wee"], bin_size=5, histtype="bar", savefig=False - ), - ) - - self.assertRaises( - Exception, - Plotter.hist_outgoing_conn( - weights=matrices_dict["Wee"], bin_size=5, histtype="bar", savefig=False - ), - ) - - self.assertRaises( - Exception, - Plotter.hist_incoming_conn( - weights=matrices_dict["Wee"], bin_size=5, histtype="bar", savefig=False - ), - ) - - self.assertRaises( - Exception, - Plotter.network_connection_dynamics( - connection_counts=num_active_connections, - initial_steps=10, - final_steps=10, - savefig=False, - ), - ) - - self.assertRaises( - Exception, - Plotter.hist_firing_rate_network( - spike_train=np.asarray(Exc_activity), bin_size=5, savefig=False - ), - ) - - self.assertRaises( - Exception, - Plotter.scatter_plot(spike_train=np.asarray( - Exc_activity), savefig=False), - ) - - self.assertRaises( - Exception, - Plotter.raster_plot(spike_train=np.asarray( - Exc_activity), savefig=False), - ) - - self.assertRaises( - Exception, - Plotter.isi_exponential_fit( - spike_train=np.asarray(Exc_activity), - neuron=10, - bin_size=5, - savefig=False, - ), - ) - - self.assertRaises( - Exception, - Plotter.weight_distribution( - weights=matrices_dict["Wee"], bin_size=5, savefig=False - ), - ) - - self.assertRaises( - Exception, - Plotter.linear_lognormal_fit( - weights=matrices_dict["Wee"], num_points=10, savefig=False - ), - ) - - self.assertRaises( - Exception, - Plotter.hamming_distance( - hamming_dist=[0, 0, 0, 1, 1, 1, 1, 1, 1], savefig=False - ), - ) - - def test_statistics(self): - - self.assertRaises( - Exception, - Statistics.firing_rate_neuron( - spike_train=np.asarray(Exc_activity), neuron=10, bin_size=5 - ), - ) - - self.assertRaises( - Exception, - Statistics.firing_rate_network( - spike_train=np.asarray(Exc_activity)), - ) - - self.assertRaises( - Exception, - Statistics.scale_dependent_smoothness_measure( - firing_rates=[1, 1, 5, 6, 3, 7] - ), - ) - - self.assertRaises( - Exception, Statistics.autocorr( - firing_rates=[1, 1, 5, 6, 3, 7], t=2) - ) - - self.assertRaises( - Exception, Statistics.avg_corr_coeff( - spike_train=np.asarray(Exc_activity)) - ) - - self.assertRaises( - Exception, Statistics.spike_times( - spike_train=np.asarray(Exc_activity)) - ) - - self.assertRaises( - Exception, - Statistics.hamming_distance( - actual_spike_train=np.asarray(Exc_activity), - perturbed_spike_train=np.asarray(Exc_activity), - ), - ) - - self.assertRaises( - Exception, - Statistics.spike_time_intervals( - spike_train=np.asarray(Exc_activity)), - ) - - self.assertRaises( - Exception, - Statistics.fanofactor( - spike_train=np.asarray(Exc_activity), neuron=10, window_size=10 - ), - ) - - self.assertRaises( - Exception, - Statistics.spike_source_entropy( - spike_train=np.asarray(Exc_activity), neurons_in_reservoir=200 - ), - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/build/lib/sorn/utils.py b/build/lib/sorn/utils.py deleted file mode 100644 index f72b638..0000000 --- a/build/lib/sorn/utils.py +++ /dev/null @@ -1,1163 +0,0 @@ -from __future__ import division -import numpy as np -from scipy.stats import norm -import random -import matplotlib.pyplot as plt -import seaborn as sns -from scipy.optimize import curve_fit -from scipy import stats -import networkx as nx -import pandas as pd -from mpl_toolkits.axes_grid1.inset_locator import InsetPosition - - -class Initializer(object): - """ - Helper class to initialize the matrices for the SORN - """ - - def __init__(self): - pass - - @staticmethod - def generate_strong_inp(length: int, reservoir_size: int): - """Generate strong one-hot vector of input. Random neurons in the reservoir acts as inputs - - Args: - length (int): Number of input neurons - - Returns: - inp (array): Input vector of length equals the number of neurons in the reservoir - with randomly chosen neuron set active - - idx (list): List of chosen input neurons """ - - inp = [0] * reservoir_size - x = [0] * length - idx = np.random.choice(length, np.random.randint(reservoir_size)) - - for i in idx: - x[i] = 1.0e4 - - inp[: len(x)] = x - - return inp, idx - - # Generate multi-node one-hot strong inputs - - @staticmethod - def multi_one_hot_inp(ne: int, inputs: list, n_nodes_per_inp: int): - """Generate multi(n_nodes_per_inp) one hot vector for each input. - For each input, set n_nodes_per_inp equals one and the rest of - neurons in the pool recieves no external stimuli - - Args: - ne (int): Number of excitatory units in sorn - - inputs (list): input labels - - n_nodes_per_inp(int): Number of target units in pool that receives single input - - Returns: - one_hot_vector for each label with length equals ne - - """ - - one_hot = np.zeros((ne, len(inputs))) - - idxs = [] - - for _ in range(n_nodes_per_inp): - idxs.append(random.sample(range(0, ne), len(inputs))) - - idxs = list(zip(*idxs)) - - j = 0 # Max(j) = len(inputs) - for idx_list in idxs: - for i in idx_list: - one_hot[i][j] = 1 - j += 1 - - return one_hot, idxs - - @staticmethod - def generate_gaussian_inputs(length: int, reservoir_size: int): - - """Generate external stimuli sampled from Gaussian distribution. - Randomly neurons in the reservoir receives this input at each timestep - - Args: - length (int): Number of input neurons - - Returns: - out (array): Input vector of length equals the number of neurons in the reservoir - with randomly chosen neuron set active - - idx (int): List of chosen input neurons - """ - - out = [0] * reservoir_size - x = [0] * length - idx = np.random.choice(length, np.random.randint(reservoir_size)) - inp = np.random.normal(length) - - for i in idx: - x[i] = inp[i] - - out[: len(x)] = x - - return out, idx - - @staticmethod - def normalize_weight_matrix(weight_matrix: np.array): - - # Applied only while initializing the weight. During simulation, Synaptic scaling applied on weight matrices - - """ Normalize the weights in the matrix such that incoming connections to a neuron sum up to 1 - - Args: - weight_matrix (array): Incoming Weights from W_ee or W_ei or W_ie - - Returns: - weight_matrix (array): Normalized weight matrix""" - - normalized_weight_matrix = weight_matrix / np.sum(weight_matrix, axis=0) - - return normalized_weight_matrix - - @staticmethod - def generate_lambd_connections( - synaptic_connection: str, ne: int, ni: int, lambd_w: int, lambd_std: int - ): - - """Generate lambda incoming connections for Excitatory neurons and outgoing connections per Inhibitory neuron - - Args: - synaptic_connection (str): Type of sysnpatic connection (EE,EI or IE) - - ne (int): Number of excitatory units - - ni (int): Number of inhibitory units - - lambd_w (int): Average number of incoming connections - - lambd_std (int): Standard deviation of average number of connections per neuron - - Returns: - connection_weights (array) - Weight matrix - - """ - - if synaptic_connection == "EE": - - """Choose random lamda connections per neuron""" - - # Draw normally distributed ne integers with mean lambd_w - - lambdas_incoming = norm.ppf( - np.random.random(ne), loc=lambd_w, scale=lambd_std - ).astype(int) - - # lambdas_outgoing = norm.ppf(np.random.random(ne), loc=lambd_w, scale=lambd_std).astype(int) - - # List of neurons - - list_neurons = list(range(ne)) - - # Connection weights - - connection_weights = np.zeros((ne, ne)) - - # For each lambd value in the above list, - # generate weights for incoming and outgoing connections - - # -------------Gaussian Distribution of weights -------------- - - # weight_matrix = np.random.randn(Sorn.ne, Sorn.ni) + 2 # Small random values from gaussian distribution - # Centered around 2 to make all values positive - - # ------------Uniform Distribution -------------------------- - global_incoming_weights = np.random.uniform(0.0, 0.1, sum(lambdas_incoming)) - - # Index Counter - global_incoming_weights_idx = 0 - - # Choose the neurons in order [0 to 199] - - for neuron in list_neurons: - - # Choose ramdom unique (lambdas[neuron]) neurons from list_neurons - possible_connections = list_neurons.copy() - - possible_connections.remove( - neuron - ) # Remove the selected neuron from possible connections i!=j - - # Choose random presynaptic neurons - possible_incoming_connections = random.sample( - possible_connections, lambdas_incoming[neuron] - ) - - incoming_weights_neuron = global_incoming_weights[ - global_incoming_weights_idx : global_incoming_weights_idx - + lambdas_incoming[neuron] - ] - - # ---------- Update the connection weight matrix ------------ - - # Update incoming connection weights for selected 'neuron' - - for incoming_idx, incoming_weight in enumerate(incoming_weights_neuron): - connection_weights[possible_incoming_connections[incoming_idx]][ - neuron - ] = incoming_weight - - global_incoming_weights_idx += lambdas_incoming[neuron] - - return connection_weights - - if synaptic_connection == "EI": - - """Choose random lamda connections per neuron""" - - # Draw normally distributed ni integers with mean lambd_w - lambdas = norm.ppf( - np.random.random(ni), loc=lambd_w, scale=lambd_std - ).astype(int) - - # List of neurons - - list_neurons = list(range(ni)) # Each i can connect with random ne neurons - - # Initializing connection weights variable - - connection_weights = np.zeros((ni, ne)) - - # ------------Uniform Distribution ----------------------------- - global_outgoing_weights = np.random.uniform(0.0, 0.1, sum(lambdas)) - - # Index Counter - global_outgoing_weights_idx = 0 - - # Choose the neurons in order [0 to 40] - - for neuron in list_neurons: - - # Choose random unique (lambdas[neuron]) neurons from list_neurons - possible_connections = list(range(ne)) - - possible_outgoing_connections = random.sample( - possible_connections, lambdas[neuron] - ) # possible_outgoing connections to the neuron - - # Update weights - outgoing_weights = global_outgoing_weights[ - global_outgoing_weights_idx : global_outgoing_weights_idx - + lambdas[neuron] - ] - - # ---------- Update the connection weight matrix ------------ - - # Update outgoing connections for the neuron - - for outgoing_idx, outgoing_weight in enumerate( - outgoing_weights - ): # Update the columns in the connection matrix - connection_weights[neuron][ - possible_outgoing_connections[outgoing_idx] - ] = outgoing_weight - - # Update the global weight values index - global_outgoing_weights_idx += lambdas[neuron] - - return connection_weights - - @staticmethod - def get_incoming_connection_dict(weights: np.array): - """ Get the non-zero entries in columns is the incoming connections for the neurons - - Args: - weights (np.array): Connection/Synaptic weights - - Returns: - dict : Dictionary of incoming connections to each neuron - """ - - # Indices of nonzero entries in the columns - connection_dict = dict.fromkeys(range(1, len(weights) + 1), 0) - - for i in range(len(weights[0])): # For each neuron - connection_dict[i] = list(np.nonzero(weights[:, i])[0]) - - return connection_dict - - @staticmethod - def get_outgoing_connection_dict(weights: np.array): - """Get the non-zero entries in rows is the outgoing connections for the neurons - - Args: - weights (np.array): Connection/Synaptic weights - - Returns: - dict : Dictionary of outgoing connections from each neuron - """ - - # Indices of nonzero entries in the rows - connection_dict = dict.fromkeys(range(1, len(weights) + 1), 1) - - for i in range(len(weights[0])): # For each neuron - connection_dict[i] = list(np.nonzero(weights[i, :])[0]) - - return connection_dict - - @staticmethod - def prune_small_weights(weights: np.array, cutoff_weight: float): - """Prune the connections with negative connection strength. The weights less than cutoff_weight set to 0 - - Args: - weights (np.array): Synaptic strengths - - cutoff_weight (float): Lower weight threshold - - Returns: - array: Connections weights with values less than cutoff_weight set to 0 - """ - - weights[weights <= cutoff_weight] = cutoff_weight - - return weights - - @staticmethod - def set_max_cutoff_weight(weights: np.array, cutoff_weight: float): - """ Set cutoff limit for the values in given array - - Args: - weights (np.array): Synaptic strengths - - cutoff_weight (float): Higher weight threshold - - Returns: - array: Connections weights with values greater than cutoff_weight set to 1 - """ - - weights[weights > cutoff_weight] = cutoff_weight - - return weights - - @staticmethod - def get_unconnected_indexes(wee: np.array): - """ Helper function for Structural plasticity to randomly select the unconnected units - - Args: - wee (array): Weight matrix - - Returns: - list (indices): (row_idx,col_idx)""" - - i, j = np.where(wee <= 0.0) - indices = list(zip(i, j)) - - self_conn_removed = [] - for i, idxs in enumerate(indices): - - if idxs[0] != idxs[1]: - self_conn_removed.append(indices[i]) - - return self_conn_removed - - @staticmethod - def white_gaussian_noise(mu: float, sigma: float, t: int): - - """Generates white gaussian noise with mean mu, standard deviation sigma and - the noise length equals t - - Args: - mu (float): Mean value of Gaussian noise - - sigma (float): Standard deviation of Gaussian noise - - t (int): Length of noise vector - - Returns: - array: White gaussian noise of length t - """ - - noise = np.random.normal(mu, sigma, t) - - return np.expand_dims(noise, 1) - - @staticmethod - def zero_sum_incoming_check(weights: np.array): - """Make sure, each neuron in the pool has atleast 1 incoming connection - - Args: - weights (array): Synaptic strengths - - Returns: - array: Synaptic weights of neurons with atleast one positive (non-zero) incoming connection strength - """ - zero_sum_incomings = np.where(np.sum(weights, axis=0) == 0.0) - if len(zero_sum_incomings[-1]) == 0: - return weights - else: - for zero_sum_incoming in zero_sum_incomings[-1]: - - rand_indices = np.random.randint( - int(weights.shape[0] * 0.2), size=2 - ) - rand_values = np.random.uniform(0.0, 0.1, 2) - - for i, idx in enumerate(rand_indices): - weights[:, zero_sum_incoming][idx] = rand_values[i] - - return weights - - -class Plotter(object): - """Wrapper class to call plotting methods - """ - - def __init__(self): - pass - - @staticmethod - def hist_incoming_conn( - weights: np.array, bin_size: int, histtype: str, savefig: bool - ): - """Plot the histogram of number of presynaptic connections per neuron - - Args: - weights (array): Connection weights - - bin_size (int): Histogram bin size - - histtype (str): Same as histtype matplotlib - - savefig (bool): If True plot will be saved as png file in the cwd - - Returns: - plot (matplotlib.pyplot): plot object - """ - num_incoming_weights = np.sum(np.array(weights) > 0, axis=0) - - plt.figure(figsize=(12, 5)) - plt.xlabel("Number of connections") - plt.ylabel("Probability") - - # Fit a normal distribution to the data - mu, std = norm.fit(num_incoming_weights) - plt.hist(num_incoming_weights, bins=bin_size, density=True, alpha=0.6, color='b') - - # PDF - xmin, xmax = plt.xlim() - x = np.linspace(xmin, xmax, max(num_incoming_weights)) - p = norm.pdf(x, mu, std) - plt.plot(x, p, 'k', linewidth=2) - title = "Distribution of presynaptic connections: mu = %.2f, std = %.2f" % (mu, std) - plt.title(title) - - if savefig: - plt.savefig("hist_incoming_conn") - - return plt.show() - - - @staticmethod - def hist_outgoing_conn( - weights: np.array, bin_size: int, histtype: str, savefig: bool - ): - """Plot the histogram of number of incoming connections per neuron - - Args: - weights (array): Connection weights - - bin_size (int): Histogram bin size - - histtype (str): Same as histtype matplotlib - - savefig (bool): If True plot will be saved as png file in the cwd - - Returns: - plot object """ - - # Plot the histogram of distribution of number of incoming connections in the network - - num_outgoing_weights = np.sum(np.array(weights) > 0, axis=1) - - plt.figure(figsize=(12, 5)) - plt.xlabel("Number of connections") - plt.ylabel("Probability") - - # Fit a normal distribution to the data - mu, std = norm.fit(num_outgoing_weights) - plt.hist(num_outgoing_weights, bins=bin_size, density=True, alpha=0.6, color='b') - - # PDF - xmin, xmax = plt.xlim() - x = np.linspace(xmin, xmax, max(num_outgoing_weights)) - p = norm.pdf(x, mu, std) - plt.plot(x, p, 'k', linewidth=2) - title = "Distribution of post synaptic connections: mu = %.2f, std = %.2f" % (mu, std) - plt.title(title) - - if savefig: - plt.savefig("hist_outgoing_conn") - - return plt.show() - - @staticmethod - def network_connection_dynamics( - connection_counts: np.array, savefig: bool - ): - """Plot number of positive connection in the excitatory pool - - Args: - connection_counts (array) - 1D Array of number of connections in the network per time step - - savefig (bool) - If True plot will be saved as png file in the cwd - - Returns: - plot object - """ - - # Plot graph for entire simulation time period - _, ax1 = plt.subplots(figsize=(12, 5)) - ax1.plot(connection_counts, label="Connection dynamics") - plt.margins(x=0) - ax1.set_xticks(ax1.get_xticks()[::2]) - - ax1.set_title("Network connection dynamics") - plt.ylabel("Number of active connections") - plt.xlabel("Time step") - plt.legend(loc="upper right") - plt.tight_layout() - - if savefig: - plt.savefig("connection_dynamics") - - return plt.show() - - @staticmethod - def hist_firing_rate_network(spike_train: np.array, bin_size: int, savefig: bool): - - """ Plot the histogram of firing rate (total number of neurons spike at each time step) - - Args: - spike_train (array): Array of spike trains - - bin_size (int): Histogram bin size - - savefig (bool): If True, plot will be saved in the cwd - - Returns: - plot object """ - - fr = np.count_nonzero(spike_train.tolist(), 1) - - # Filter zero entries in firing rate list above - fr = list(filter(lambda a: a != 0, fr)) - plt.title("Distribution of population activity without inactive time steps") - plt.xlabel("Spikes/time step") - plt.ylabel("Count") - - plt.hist(fr, bin_size) - - if savefig: - plt.savefig("hist_firing_rate_network.png") - - return plt.show() - - @staticmethod - def scatter_plot(spike_train: np.array, savefig: bool): - - """Scatter plot of spike trains - - Args: - spike_train (list): Array of spike trains - - with_firing_rates (bool): If True, firing rate of the network will be plotted - - savefig (bool): If True, plot will be saved in the cwd - - Returns: - plot object""" - - # Conver the list of spike train into array - spike_train = np.asarray(spike_train) - # Get the indices where spike_train is 1 - x, y = np.argwhere(spike_train.T == 1).T - - plt.figure(figsize=(8, 5)) - - firing_rates = Statistics.firing_rate_network(spike_train).tolist() - plt.plot(firing_rates, label="Firing rate") - plt.legend(loc="upper left") - - plt.scatter(y, x, s=0.1, color="black") - plt.title('Spike Trains') - plt.xlabel("Time step") - plt.ylabel("Neuron") - plt.legend(loc="upper left") - - if savefig: - plt.savefig("ScatterSpikeTrain.png") - return plt.show() - - @staticmethod - def raster_plot(spike_train: np.array, savefig: bool): - - """Raster plot of spike trains - - Args: - spike_train (array): Array of spike trains - - with_firing_rates (bool): If True, firing rate of the network will be plotted - - savefig (bool): If True, plot will be saved in the cwd - - Returns: - plot object""" - - # Conver the list of spike train into array - spike_train = np.asarray(spike_train) - - plt.figure(figsize=(11, 6)) - - firing_rates = Statistics.firing_rate_network(spike_train).tolist() - plt.plot(firing_rates, label="Firing rate") - plt.legend(loc="upper left") - plt.title('Spike Trains') - # Get the indices where spike_train is 1 - x, y = np.argwhere(spike_train.T == 1).T - - plt.plot(y, x, "|r") - plt.xlabel("Time step") - plt.ylabel("Neuron") - - if savefig: - plt.savefig("RasterSpikeTrain.png") - return plt.show() - - @staticmethod - def correlation(corr: np.array, savefig: bool): - - """Plot correlation between neurons - - Args: - corr (array): Correlation matrix - - savefig (bool): If true will save the plot at the current working directory - - Returns: - matplotlib.pyplot: Neuron Correlation plot - """ - - # Generate a mask for the upper triangle - mask = np.zeros_like(corr, dtype=np.bool) - mask[np.triu_indices_from(mask)] = True - - f, ax = plt.subplots(figsize=(11, 9)) - - # Custom diverging colormap - cmap = sns.diverging_palette(220, 10, as_cmap=True) - - sns.heatmap( - corr, - mask=mask, - cmap=cmap, - xticklabels=5, - yticklabels=5, - vmax=0.1, - center=0, - square=False, - linewidths=0.0, - cbar_kws={"shrink": 0.9}, - ) - - if savefig: - plt.savefig("Correlation between neurons") - return None - - @staticmethod - def isi_exponential_fit( - spike_train: np.array, neuron: int, bin_size: int, savefig: bool - ): - - """Plot Exponential fit on the inter-spike intervals during training or simulation phase - - Args: - spike_train (array): Array of spike trains - - neuron (int): Target neuron - - bin_size (int): Spike train will be splitted into bins of size bin_size - - savefig (bool): If True, plot will be saved in the cwd - - Returns: - plot object""" - - - isi = Statistics.spike_time_intervals(spike_train[:,neuron]) - - y, x = np.histogram(sorted(isi), bins=bin_size) - - x = [int(i) for i in x] - y = [float(i) for i in y] - - def exponential_func(y, a, b, c): - return a * np.exp(-b * np.array(y)) - c - - # Curve fit - popt, _ = curve_fit(exponential_func, x[1:bin_size], y[1:bin_size]) - - plt.plot( - x[1:bin_size], - exponential_func(x[1:bin_size], *popt), - label="Exponential fit", - ) - plt.title('Distribution of Inter Spike Intervals and Exponential Curve Fit') - plt.scatter(x[1:bin_size], y[1:bin_size], s=2.0, color="black", label="ISI") - plt.xlabel("ISI") - plt.ylabel("Frequency") - plt.legend() - - if savefig: - plt.savefig("isi_exponential_fit") - return plt.show() - - @staticmethod - def weight_distribution(weights: np.array, bin_size: int, savefig: bool): - - """Plot the distribution of synaptic weights - - Args: - weights (array): Connection weights - - bin_size (int): Spike train will be splited into bins of size bin_size - - savefig (bool): If True, plot will be saved in the cwd - - Returns: - plot object""" - - weights = weights[ - weights >= 0.01 - ] # Remove the weight values less than 0.01 # As reported in article SORN 2013 - y, x = np.histogram(weights, bins=bin_size) # Create histogram with bin_size - plt.title('Synaptic weight distribution') - plt.scatter(x[:-1], y, s=2.0, c="black") - plt.xlabel("Connection strength") - plt.ylabel("Frequency") - - if savefig: - plt.savefig("weight_distribution") - - return plt.show() - - @staticmethod - def linear_lognormal_fit(weights: np.array, num_points: int, savefig: bool): - - """Lognormal curve fit on connection weight distribution - - Args: - weights (array): Connection weights - - num_points(int): Number of points to be plotted in the x axis - - savefig(bool): If True, plot will be saved in the cwd - - Returns: - plot object""" - - weights = np.array(weights.tolist()) - weights = weights[weights >= 0.01] - - M = float(np.mean(weights)) # Geometric mean - s = float(np.std(weights)) # Geometric standard deviation - - # Lognormal distribution parameters - - mu = float(np.mean(np.log(weights))) # Mean of log(X) - sigma = float(np.std(np.log(weights))) # Standard deviation of log(X) - shape = sigma # Scipy's shape parameter - scale = np.exp(mu) # Scipy's scale parameter - median = np.exp(mu) - - mode = np.exp(mu - sigma ** 2) # Note that mode depends on both M and s - mean = np.exp(mu + (sigma ** 2 / 2)) # Note that mean depends on both M and s - x = np.linspace( - np.min(weights), np.max(weights), num=num_points - ) - - pdf = stats.lognorm.pdf( - x, shape, loc=0, scale=scale - ) - - plt.figure(figsize=(12, 4.5)) - plt.title('Curve fit on connection weight distribution') - # Figure on linear scale - plt.subplot(121) - plt.plot(x, pdf) - - plt.vlines(mode, 0, pdf.max(), linestyle=":", label="Mode") - plt.vlines( - mean, - 0, - stats.lognorm.pdf(mean, shape, loc=0, scale=scale), - linestyle="--", - color="green", - label="Mean", - ) - plt.vlines( - median, - 0, - stats.lognorm.pdf(median, shape, loc=0, scale=scale), - color="blue", - label="Median", - ) - plt.ylim(ymin=0) - plt.xlabel("Weight") - plt.title("Linear scale") - plt.legend() - - # Figure on logarithmic scale - plt.subplot(122) - plt.semilogx(x, pdf) - - plt.vlines(mode, 0, pdf.max(), linestyle=":", label="Mode") - plt.vlines( - mean, - 0, - stats.lognorm.pdf(mean, shape, loc=0, scale=scale), - linestyle="--", - color="green", - label="Mean", - ) - plt.vlines( - median, - 0, - stats.lognorm.pdf(median, shape, loc=0, scale=scale), - color="blue", - label="Median", - ) - plt.ylim(ymin=0) - plt.xlabel("Weight") - plt.title("Logarithmic scale") - plt.legend() - - if savefig: - plt.savefig("LinearLognormalFit") - - return plt.show() - - @staticmethod - def plot_network(corr: np.array, corr_thres: float, fig_name: str = None): - - """Network x graphical visualization of the network using the correlation matrix - - Args: - corr (array): Correlation between neurons - - corr_thres (array): Threshold to prune the connection. Smaller the threshold, - higher the density of connections - - fig_name (array, optional): Name of the figure. Defaults to None. - - Returns: - matplotlib.pyplot: Plot instance - """ - - df = pd.DataFrame(corr) - - links = df.stack().reset_index() - links.columns = ["var1", "var2", "value"] - links_filtered = links.loc[ - (links["value"] > corr_thres) & (links["var1"] != links["var2"]) - ] - - G = nx.from_pandas_edgelist(links_filtered, "var1", "var2") - - plt.figure(figsize=(50, 50)) - nx.draw( - G, - with_labels=True, - node_color="orange", - node_size=50, - linewidths=5, - font_size=10, - ) - plt.text(0.1, 0.9, "%s" % corr_thres) - plt.savefig("%s" % fig_name) - plt.show() - - @staticmethod - def hamming_distance(hamming_dist: list, savefig: bool): - """Hamming distance between true netorks states and perturbed network states - - Args: - hamming_dist (list): Hamming distance values - - savefig (bool): If True, save the fig at current working directory - - Returns: - matplotlib.pyplot: Hamming distance between true and perturbed network states - """ - - plt.figure(figsize=(15, 6)) - plt.title("Hamming distance between actual and perturbed states") - plt.xlabel("Time steps") - plt.ylabel("Hamming distance") - plt.plot(hamming_dist) - - if savefig: - plt.savefig("HammingDistance") - - return plt.show() - - -class Statistics(object): - """ Wrapper class for statistical analysis methods """ - - def __init__(self): - pass - - @staticmethod - def firing_rate_neuron(spike_train: np.array, neuron: int, bin_size: int): - - """Measure spike rate of given neuron during given time window - - Args: - spike_train (array): Array of spike trains - - neuron (int): Target neuron in the reservoir - - bin_size (int): Divide the spike trains into bins of size bin_size - - Returns: - int: firing_rate """ - - time_period = len(spike_train[:, 0]) - - neuron_spike_train = spike_train[:, neuron] - - # Split the list(neuron_spike_train) into sub lists of length time_step - samples_spike_train = [ - neuron_spike_train[i : i + bin_size] - for i in range(0, len(neuron_spike_train), bin_size) - ] - - spike_rate = 0.0 - - for _, spike_train in enumerate(samples_spike_train): - spike_rate += list(spike_train).count(1.0) - - spike_rate = spike_rate * bin_size / time_period - - return time_period, bin_size, spike_rate - - @staticmethod - def firing_rate_network(spike_train: np.array): - - """Calculate number of neurons spikes at each time step.Firing rate of the network - - Args: - spike_train (array): Array of spike trains - - Returns: - int: firing_rate """ - - firing_rate = np.count_nonzero(spike_train.tolist(), 1) - - return firing_rate - - @staticmethod - def scale_dependent_smoothness_measure(firing_rates: list): - - """Smoothem the firing rate depend on its scale. Smaller values corresponds to smoother series - - Args: - firing_rates (list): List of number of active neurons per time step - - Returns: - sd_diff (list): Float value signifies the smoothness of the semantic changes in firing rates - """ - - diff = np.diff(firing_rates) - sd_diff = np.std(diff) - - return sd_diff - - @staticmethod - def scale_independent_smoothness_measure(firing_rates: list): - - """Smoothem the firing rate independent of its scale. Smaller values corresponds to smoother series - - Args: - firing_rates (list): List of number of active neurons per time step - - Returns: - coeff_var (list):Float value signifies the smoothness of the semantic changes in firing rates """ - - diff = np.diff(firing_rates) - mean_diff = np.mean(diff) - sd_diff = np.std(diff) - - coeff_var = sd_diff / abs(mean_diff) - - return coeff_var - - @staticmethod - def autocorr(firing_rates: list, t: int = 2): - """ - Score interpretation - - scores near 1 imply a smoothly varying series - - scores near 0 imply that there's no overall linear relationship between a data point and the following one (that is, plot(x[-length(x)],x[-1]) won't give a scatter plot with any apparent linearity) - - - scores near -1 suggest that the series is jagged in a particular way: if one point is above the mean, the next is likely to be below the mean by about the same amount, and vice versa. - - Args: - firing_rates (list): Firing rates of the network - - t (int, optional): Window size. Defaults to 2. - - Returns: - array: Autocorrelation between neurons given their firing rates - """ - - return np.corrcoef( - np.array( - [ - firing_rates[0 : len(firing_rates) - t], - firing_rates[t : len(firing_rates)], - ] - ) - ) - - @staticmethod - def avg_corr_coeff(spike_train: np.array): - - """Measure Average Pearson correlation coeffecient between neurons - - Args: - spike_train (array): Neural activity - - Returns: - array: Average correlation coeffecient""" - - corr_mat = np.corrcoef(np.asarray(spike_train).T) - avg_corr = np.sum(corr_mat, axis=1) / 200 - corr_coeff = ( - avg_corr.sum() / 200 - ) # 2D to 1D and either upper or lower half of correlation matrix. - - return corr_mat, corr_coeff - - @staticmethod - def spike_times(spike_train: np.array): - - """Get the time instants at which neuron spikes - - Args: - spike_train (array): Spike trains of neurons - - Returns: - (array): Spike time of each neurons in the pool""" - - times = np.where(spike_train == 1.0) - return times - - @staticmethod - def spike_time_intervals(spike_train): - - """Generate spike time intervals spike_trains - - Args: - spike_train (array): Network activity - - Returns: - list: Inter spike intervals for each neuron in the reservoir - """ - - spike_times = Statistics.spike_times(spike_train) - isi = np.diff(spike_times[-1]) - return isi - - @staticmethod - def hamming_distance(actual_spike_train: np.array, perturbed_spike_train: np.array): - """Hamming distance between true netorks states and perturbed network states - - Args: - actual_spike_train (np.array): True network's states - - perturbed_spike_train (np.array): Perturbated network's states - - Returns: - float: Hamming distance between true and perturbed network states - """ - hd = [ - np.count_nonzero(actual_spike_train[i] != perturbed_spike_train[i]) - for i in range(len(actual_spike_train)) - ] - return hd - - @staticmethod - def fanofactor(spike_train: np.array, neuron: int, window_size: int): - - """Investigate whether neuronal spike generation is a poisson process - - Args: - spike_train (np.array): Spike train of neurons in the reservoir - - neuron (int): Target neuron in the pool - - window_size (int): Sliding window size for time step ranges to be considered for measuring the fanofactor - - Returns: - float : Fano factor of the neuron spike train - """ - - # Choose activity of random neuron - neuron_act = spike_train[:, neuron] - - # Divide total observations into 'tws' time windows of size 'ws' for a neuron 60 - - tws = np.split(neuron_act, window_size) - fr = [] - for i in range(len(tws)): - fr.append(np.count_nonzero(tws[i])) - - # print('Firing rate of the neuron during each time window of size %s is %s' %(ws,fr)) - - mean_firing_rate = np.mean(fr) - variance_firing_rate = np.var(fr) - - fano_factor = variance_firing_rate / mean_firing_rate - - return mean_firing_rate, variance_firing_rate, fano_factor - - - @staticmethod - def spike_source_entropy(spike_train: np.array, num_neurons: int): - - """Measure the uncertainty about the origin of spike from the network using entropy - - Args: - spike_train (np.array): Spike train of neurons - - num_neurons (int): Number of neurons in the reservoir - - Returns: - int : Spike source entropy of the network - """ - # Number of spikes from each neuron during the interval - n_spikes = np.count_nonzero(spike_train, axis=0) - p = n_spikes / np.count_nonzero( - spike_train - ) # Probability of each neuron that can generate spike in next step - # print(p) # Note: pi shouldn't be zero - sse = np.sum([pi * np.log(pi) for pi in p]) / np.log( - 1 / num_neurons - ) # Spike source entropy - - return sse diff --git a/dist/sorn-0.6.2-py3-none-any.whl b/dist/sorn-0.6.2-py3-none-any.whl deleted file mode 100644 index 81a09a62197b2eacc07f9996af33d5e3a67e4c95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21729 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 diff --git a/dist/sorn-0.6.2.tar.gz b/dist/sorn-0.6.2.tar.gz deleted file mode 100644 index fe9329a87666565b91d5df1081439454574655a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22784 zcmV(;K-<3`iwFoaB{^XN|72-%bX;?9a&9d!E;cSQE_7jX0PVf~ciT3yFx;PWPW}f} zIejCwvMl+t^@+skzxq_4CVX~xcj+&D+`sg_wbg3vY;0_7?QDM4Y&CZ_ zH@{-rUp>NSnxsL@*ngeF``0TJT?tOyw-`UwITmOyL2Cjdz+1lCM zXl`%7`U4_te8rlp_5b9b*GK2NzwnkLh$I2Z>LF$*SG;e^Lykt8CVFp;n!kNMS%4WLHKdv(^2IcH&?b%#Mb;B}UU zESSt##Nz}igjZ=GCSo#R0qa7;PoF^1bO>;gu%F%pF^AGU79>g76#)Qby|6nS^GO<{ z*kE6bc*3gbkh98}s!^#?D?J{Jo<0#1hA;Iyb|cbZI89m1lQb4x1X5>W(j85E*bDu7 zB*sEDfz@d~lBZ7q>@UI^1hZNBH-6rW)E zemEM1H#oX(IOz!-RI-N)cMdOutMHoBc;uQ-!W6nIdx6Uk<%_FcCBtAeVpm+v8ng^^ z9GF9ju?Ij?lT-vF7KJf2=?=9)UHJKsoxOQ^{_FnfAv-!_C#P@zb@bx!1*`0z!S70) z{d#o%^P6AJ8I(BPKR$oU-n?Y{$8Xs`kB(o|+2L;|r-x@}?9J)ZCr7VOUL766!=vMa zSHHYCI{u0M11cWB0rWlsBnPPHZy2_yU>zNvA+*u zLVfw>lD{SlOll@;{0H@eRcJSup?dj1^KcBOo-@JNp2oL{p2z}fC$E!ox7L4oQ)&9}z zI(xDIdjF?Gs`UoI!0@n~>>&H~=Rr4P(SPg(`0d3RFBklwfeanYAoX1b_c>c9=nAFAn!#0kAWyOrzAL zVO76ceg0G*WB)(-=TB>Z)NekC{8zsJw{|yoHXQk{wY$|??f;Lm|MN8H1!=JMFW78= zQ}3~jM(gPl*z5ToL*#h+Bzr?>0@OYokArx&$IkeuzxE~`fHd$s?l7mkJB?%5p4l;< z#=!lLd3qDZA7H2YIUMt~C>Q|HVVXwC-un6gcH8MyqZ^LbNyJ#2Op#@B(W{Ev7c+!nUl+qxd zK4I$f>@EY;mGWQOLL?;CGqQQPiu*^6B$p`+S6-i-Ele(O1z37P;M#Nic6+X=h_30%Rmn7>{bNwplL1W4F zptelyfQTTeSzYLcJ>Ctk^ZM!mj%qM;wwQH5fJt|_K8BU$@jB?M(a_t8c|h#;Us$TC zI~C?M!@8^kF`$-~EWc8+d+YywEqCL#T&CKCuK9mc8yd7Vyp!3<5mRmFZOSPFHFSo6 zq8ZJLGlkwVHt++OWgzX@2zKhL5wM!Pwipi@$x!f7kNPSn0axXljV9GtqY7(M>MBQ} zDCw_LNq5z5CZjOu8A}!MXx2!9&{bp9UEgc9hCW<)bCg&3olnAENUJUZf^fULz0=(O z-sx?jpj?MuDDo?06Q$^RH)wg1oa{WWp$uUG$G z6Q^rw%wO$y-v01`pT3R%&xfC%f&2z?#d+91O*h{DPn5n+4_aGxw-bjV--k)k04fOI zOM(p0+Oode>uzoB_IHD3ueICT-QBv{{-OIl=dJDT#>QrMV_kxA25>)EH6Jtm+i(l` zI_$CS$}8SaD?gH?a0+u1Uxy-Awi(zquEWt(OI?W;UU(nm$pCiDyvKQRTMT5P+>!kHl=oDbE8>dHv%|LyRx;}gx1%CAALiA_6*Pp=658;Nneq7@7_fu zkmQdd5rIHM)s((s6!yVD!`Dc_27r=K0&GV>O|oJ|!vKB;XwHL;brw!RbeqXt_9mE+ zNlmweOCZTpGey>qm5HtgqQoeSXxo?;jMmFtdBTr(tw^L*0q|6!zr zyPnan`jQi5PZ5c!7x3-!dC5e;@P9!u#(eV3l+;g#LBf}89E5b3ECa~&%$+5_V*2)SR$zk#O!E}_e;S4xk1UNb7zz-6sy0Aeo zO%f4IQ1ye_lTetGj2x7AdZL@w*&%3Zb#_{32hi7S>Kch^xPvoW%?2aC)LDe9)_$JL z<>4O>WDuzs!`p_ zehK|@t63{0v1c$>%8lcYlZS{3^g?xcY@*%qoAbjx_F_spSgK*uqSqx3ASagU6ibHT z6jTpT?NB8|9|x=AjMrEX6({1%AbwztR)Y!N&>#|=0A2kG21<`}>S+1Zo~0u0a*&(~5?DUrNcS#Ow^wN}Iie9rC`kV$ezJo**Wi*dk!* zWZJft;ePN&!T72dbhrdtXFLs{hrOuXY&4rKHjfZA#lD%Qh+>9aF){n{E}z7AjIFU3 z(iMpUiAB%shD4}@Ro?-fzN^E(0{<2KT@A?l&@{`*h68O8s=oW}9ht!lbDUKKcHQc` zw^-&J1VbPLpn~gQG)0H%yK`*pT)gw5G4%iN9rSu$^Jp}jq$1`_v2q*@+A_jk3CJS^ zS>E0IfILNzxlQT;ARZvZobwVS?lNH9ERDG#BWjQ`9(0F-7_5D_>xoNS8 zfIJH7N1Odo?tS==RWB7OOr}#WlhOCk7KGMVsb9pLCf0^927POoLzslu*6SG$VwS(g z0a))<>;aEe2mXf)33d}&OVOPK)oruUvdi>B!HjnPXf+$#8{fCIVM5KBT?h0@tB%GB zfJM4+35*=Xf0j^U01?o2fKe4TFFj(x7`WsJR3jDQke6z(qdqBdH!wH2|7UM`hTdI3 zNsv$jrKd?U2w*}%y@>h0r>Ng)jTp*gyOU!W3Ecp*?7W zYy{X&1N0q&sO;~c^^wYkR4*od^mfQ#8dVAF!(_n-0cafdrd{4!U;uvlbdx&zSa3lPv?I$Z3%?H}&xK|0s`}(7 z=yJ22pEyWI@b_qv^3g~P=)D4PWDNZz4ned*1N+k_pd6<7W{>@H{LkYzzaBq*a)9nT zbkD>fl^VLewDF=haTB(MeI#$Y@Oc=CF1|mLJ`CvA`k~=pfHy{O&0gAYlb;8#QzuK;nU=)HjoN=?U3-?>6#`|eeC?p|g0?p40GoVaTT(tfpe4(jP1OZjcO zJ`Um!z3^tTicl$y|2RF|fARVggnurI|JdBw-lX`C?d{FojaC!mKQSb9760)FpQlgm z6y|`jUaPQ%RanE9AJ*{n$tsLt6~^F%G2s6`W4OZS3RC!th9`WIu!P4CM|jvUgijrQ z@Tg%2f3k3cFEz~I>5~TzEBNxl2|$i@!v~%|F&t3EFafeQ$#XD|9u832wI$vDs_zB& z{-<~AFX_&=1~|uUPXo!_^nUle1n~8hTLo`4{3g ze^_^UnVVc82L0o8gWuQveGj*{LeZj|+d#^4xAqt1&R!CeTN;7yi`~!}VlHqy|C!v))ZdSDFB6E5b0-ssk8&Rqgokt&%awNA!+(9ZupD;o1}3c6 zZr}U4d++Mzy}x_6#I0NA&YkDRo#(#2i`(|z?%MwtLTAAp`$f57|B~)k{NJbc|A1UE zjT+tl;Boyw@Y#0H|88t;ug?EI%IAU)1|3AiOHh1*K}ULO+D!iHAz8Lr^OK?9>htCJ ze}2%6u}S{l&CT5v{XfP>>8oL)l~_aq^5$g+9`K1&2!BbXyHz{v zaHm4)jdm^>K0rx%8dNw@{xvd=g%jW=xnj~_>?nPnunA~wb=d8?0a~6W;SFomK$4IO zZ~*WlHbNN|0U8a^uDelV0%5@RDx?D|okiJBZI(_!W}v(}AQFR&7Th2>Ow%(V;ou5n z3I)|LX+%LfY`hOe{-Y;iEMM((u)R(P^c)GG zM$&DSBw3oss)UFy+Z80fiqZ?U8Rk=>%cI5E!f-LRC|-=s4H$!QsV;$Jo!h@lO}DzV zazrQX3yB1mdT9bs^C_vvw(neH=gu{D?_A@1T|;AqO2*YuScPI+ z&^Ao6%JkkrT?&W~af{62l{%{cUi8Hv3gU#v_!|gAzDRg*MHtH}5wP1J!EZ`wzs(Tx zcM)}xpwHXd%vM{;ihox54|1!LPBv-h4cbx~&E{7Alp-+Ih1xag4GBq4q8%_He z0^72m3FS}Iv;TMWW{Ix!Txfl!pRXtHNHs^ z=aeDCt*h>YVF{Y0O3tqGGpqmFu#0xDNjuStBR6$(RQdoW@>In5(tIVNC)1Rjr_Agd>1Ln@BTEzLZ!tsUGv1HrYtuC%SDg z#_HG%XBd!&D3%~Lwkz|Nv2E@Rje(lPjvR{7;cC&A;31*99=QG@GKQeP0B3KvY+yBW zHJ?d(HP`P~;A?pSKJwy@^MxG{9#1`k!z_@%_NhSNg%24*A}X_uxblcyFe%C zI#0#&1fpYBI@f3fje*mUEK>)0mA=f!`Yhr_)=t=Ogzs-3ga{^F@AdhnQaqUD)Dgux z^BeXfm2bx{4A$;Xek{xeHCqyt(aju`gJOyW!{!wx)hgo9hKWNn`?XN73rGTZG|M%k zJgSzEORqSsHfHPZ%6l3zEvkXhQ#?i`2yx&#hy{kZB-VkgTwugwNO@fe`zU36(gkIU zf>INT7xtfxL(4LNlW+*-(2zgkMkgrXI8}ioJ*f|(06R&IhO;s0=|DQ@bdVA{9oj+d zy)v1C%u=m2vLePbW9bH|dXgBYl$yxEH*_XByh7IDSsiZIDd$mC(NGc{(S)Gs*ZHd& z;b~jUYgxGM=0_2Y#z*&WTvxVBY!}(*C>albLu|xb1%XSO$SOO zbTQ1fGHAivDP@JRQyJq4jw?*@;HC~SB=(d?GP=ODDM12~zW|64xd>07_Bh{PJs`+p zk%53;4v65^1(DM+;y%%E9ppsVz<>u5%dN9p^GA1@hW)-W0wjBE1QM{bzwJj6p1MWC ziKipZ_F#V)C?=JidC~eFXdfxGr!AOOjM3!EBh_-PlwuiENZm5(@|>=h2ux}>3lgeVy*1GEHtn?F z?d`=LC>NLJbN#esJcY(P_yYbm@XDI1qhrx}(rHz^h6W87p(Y~FiEUJ)RAgP2n-1kb zq&%9A>WELO)aFI=vWDXYRK=iN368^=OjE zRch^z_P_Uloq&;U7wBM8IxOjsvQ)?>-(OycOZGJx|ER5lA@6>$PGS4%Yx6{EL}cO^ zB|7=b_xK3dxL!ni!~2%~-}2!2>TQah{FcxKdd$}3H}wpj)XMw#OVraRa1^d=)%5zzA1Ouwp_{ zpLm?xD=z&PlMHMW-oWl5sl71udZJBJNQ7;6s6EXcBa}0iDd`#PKx)W*2D6YBiddGzQgr&<$gB zfQ@E0IL$pcU^&!dYinyi=6(`c#vFr>z_RysLy&T}E=5xS;iPPc$(r60-(zA4Zr=$2i(L12HLS zoB3{Yo?~$wOCA-a^62bd!l}rl)4Gyd;^jqDL!GdsRfE^uPjEG?XlHIG61i&{nha$W z(G5O~Xls~R<{a*NOo8|#rli1v=T3yhOD%!A$XB1upPg(rrTlFGo7J=|ZUPhc6s?U} zQj)vyNbZi=7Vn0m>3H&A_Y~8`y~OnTM@Z@7^GNA( z!K&p_`I=+7`IfK=#q)3Hi1IBG-lgacKfYrvV7|E##q}k5y=k9Ez!#Q)FA4&_czgk0 zEGgiNPb}bzr3HL(2La!h&@%BP%Ocjvr*W>tvft~`kv@Y$rBg~>mg}rkhDxp&tzw#N zadDAg^HFk*RoU4%qE5GPpr5RRbviiSsyRUlR34ZKu@d;9!hu{D@{7>S)0WS$Tkc>O zhDiyg>v_tvq1d4eBvV;DZ_?wpl!?Rk;isMHB->g#suZqut=MtZA-rf>^ltM$BvU+6 zjiE2%>-KwRwug951Or$0JdC(^+d(MMD>cR4VJN!1s`^p$@@`!VXbpLH_Oo@l=Vvlk zphbOfhl9oh1JdP%0p~K08=EJsF7^)^bUNr!L*Gw4pkum;Qs>Amq#Z$MQO>`0yJd&$PS;0+z6SSvaWyKn$QElb1Vr}IP zuwwY?3nRtamBYhIeQtPHTg98yy&+q&-7L7zC}a)t=#vdnEH%iZ2OOj*8RXF?TS>9h zN*+DnN(yHsjbU=GhiUR^^hk)&nkjVvIpO2y7re7Zn-OOIl+r9M=@^w z6;INnO08IGUKXWq5|u;gn?z<%`X;HcZ&K7yW_FVDNEQH@g}oW~YUz@_jk^}oV+ zpqc&3Ck>L7vX1?%tN5{c79ymBgkBRK9AGZ%uFMl{hYG1Tq#kC1P(Ham!=~QTX~K-; zNY%)2wUAh4r4UQ9o;$Lk6fLvha#aVI|A`}hnj_EpS4BAzW5pahSl#hw7)qh8tKk)e zNOz{>5lcM9Iw$x@r8!fl3UniT3_A#mh#B=v@!J=j0GJm3cb6-~YnQSHnA4oX8YTK-QUBggFuA(s3tH7zCKj3M)q{~H3g9r>MdN_R z3;kO7UJG4Y_-dc4x2gA~f}HvQ|0dtvdS1PiudJPgLi+DSVqdRF>@P}U8A;@}6AE8- zjVFpk&_d~uiOi!?azA8|a&)ze3OuS@&Z8o;sPGpRMX16L^}JB>yrf{E^>csYzq#dp z^A>yS7JFN{Tttm}IxC*zGnBK{pfF4=N9W^&b|Bcsuc9!sqT&CwG>r6%_myxN9@w~UhrkT&Mf9!+HA;B;~Kp+S_@=y1;H*uW#hGQUkkIg;2sTk9$PfA zs9^N)g$<@+S-dGa2JR3!Xe@W?M!fq@t{KZ)THd#`cU;^&knnZx?4T_U2;8%_=f>g` z4o93D3v0^N=CT?4mAO>f@}1d+OcSGAw%jq>fzsBZWph<{Q+|$Ln+T*MCrFu2g_imz z)3dsEf9@6HWOw{t@lFqb1HULE&Y^w0&*%irQF%bA;{4g#+cV>odz+WDhhIkMnLINe zUpg&C-sKT_UgCC5MTiPf%&$c_O_s)m$Bb}$fE7Q{S`ck;|-FnEl}> zKl-Qw!RZ1Dbh?a10{dWhxX1qZ4;nQ7_)n!F(Oy8P@=bt5*V1A1k?>ap(PBKttAdL*4P;NlFG%61SmVv!F zgK<0?rLjo{*2}S@crY`ZE?q0;JGDn_|r7k&LC44Ib-Ok4Utvf7lX}ykB{ft7$ zRrGi4qc|wDME82?2Own ziVIyh*7`+jS$7rHA9KUE7A1%9lfF7iT^ehMusQryp>|wc4d3}H}#Zq*6kXhY}10JikNe5l=|}d>_=Lo z?rP%rc#bLV&oO63jyb0jj#8|GL~`QS;f-tS`D^<9bsZZ zypAl>MjVCNU`#6!@UcZe!>m!f?T(ORCHH++x$mxAwkX}X0-ci2{w%E5x=eRnMgXc5 zx+?dVz5=(CMJ#64zN{ieicj&<4#z68E-$f_uaL?05dqOu5kZ&e^3tMv$C0Ddg{IY9 zn$tHUo7O_b_8RPkyZ|^smWx3@Rvwx>+|h#K20PkN%;2I%c^sq#(b@r$x)lWOKlQSp z=SDn3mGTQ3jmgM{Jo~GWcnUxzp1YFm###+KdK;IldKHEvtCgd^yoy*AxWUW3uQ&^O zJ>Ik7VX;QJ#a2ycr!nC@}hKs%NW@s z;6hfeGV3PpQV~&dja*PuLBMr~7f`@3vjYh0MGCh9iR(V9Bwtst0MW`GLt-jk82_8)=ue|q-+Wo!Xv@_Qc-apSl_ zO3WikZ9nM=RZ#a7b3rMop67L+W8M75`(pHj6))=Zn7c8g{!$-zP=rb-!fmsQOLWKT zpK>=~e3WqY%p8N<7&=(XIT<)9oII^ero~705#|N&#r*f8SU)?eT>3_oz41WPxeUE8 z$XM)uQ>-9Q*%cp)(#K-r1>$*3Syl>9^(8Roi%UB}&j${Bu*{VKm zM+y15PMjmv^|~(y0NqvLWchaFvfEBKKP1*Jp0M-qn08LF`6;osMXGM{7IiMqVtYpH z66Kc5q=KS8(q0ss^LDvnr}K*~5cQG0*yB16*+8uVvQ0TAy?xLI_7?|}J9IW`AU5Qw{j*CToc#Xk zjJIwDc7VGz(<*7ERcywJ>-L`=DR4Q8!d|YNSgu?l>xF}hDiurHQ}e6`;eaqMLrbagbL%8F_#2|AC|;o4y_rF$7aY~Y<@lU}t-vGbRVGLP0M$f)K) z^HbY%W4|WcFlnq&?#43K9-5`)r@!a&=Wq1yTl#lSlt9gQomFaP(vsSgduxb#I)FY+ zjP2J^v=SHgZ8iO_1!z5^zokLh1if>c8WdyA$`S!t4>mm|LEo~Av(hdu5qMx^GLc;q zM%OHsyl~CbF$ZdtyK&Wn>+G=6!^4*1qQZ|yr- zjd?f=^2&2mJDy`ULzHt2eQmD{X;BZ%&vjECTfc~)aXjDB?BsoIc`;b4(e!a7-}M~D z-iu}XeqQXlh^psy4}FuiIEfw2;>XRQFBPFI>FJX2FQmBBN{Zri%w0<1EahzRqI#vk z!u)1I!YU2)uZ5(3IK9coOmC9UR@>|k z#|VL^T=)Fa)xA5Yv^Wr}q z_qaQib-9FfxkPoj1a+@OX&#gzCptGA=K639IoC}?0^KTF<`&n5ER#rCn9t+;%+Iak z){c88Guy8G<@fREd=cK9mE(TpxPM5;J@JGG_S$EB=Fi}>|CIdy^E&(Qu@|g7{rA`x za+bVuudm$eU!;3|<(gi(ra!_pJ>Q-DkPhqzjpBI7coMZ|TZftCv)V(%j(o{(>Bn;G zuUyxk(RD3-)r$x4@xE+>oc?z?v{&xzl{>tz7(K?NnnSxTa}zN3YY z?f-7Cyx|WvSzm&0y#2_Y@;Ux-?RH+||1S8uAK%+OH|w=$*InBCUFHG5hZp=Fp74+L zhA-<8zq42T{+{urz2kTGkUxl*ywp>EA8+}6JmwGRHTSZNWxeNj_Mk80MK8Nf)6*TX z{B)Xr_a!9mXZ~#iq}eeVp_5?jnF`Zlzw+v@y!tDzK6&+(Kc9R-%CiqxksXZ3jPv;+ zd3YnxVKxB@>vU+B?sSmmIvu@7Bbj}*`uycT=;~Z+Hg*~t>-cpY)QyzDM56lv@55FV?n|u(bFf{#cB=whp8f`(}}d1|HOg)1e7*2FF8%c zDA7evMj^BYI~N+o1TsEgc!n*|sHm%C+^en`9CAJAR;SMcV4<}8w3^Ui!!m7Lh+H*` z>Wnh^?dWp2xrY(BAo;z+h)=2&kVK*p&zN%~`Y@{0StY)z)NoGvif2}f{(ZW{*5Lhz zx7}CV5}1 z8GQIoIq94=bvDW}U)aGOZR$n6bU@ME>9^+F^PWsLZ6_wC7+=xe@YUt*ji&GUwB2&e zw)MZpESB-APd%N@Pd%S%qiA@8x}2$$f9gw+*vHJ|v~6uebEk7Dbj!GPB)8O$;@0+a zwMjRIut9giX*vk$(qq_gJhP~+Al3}1tBdOF3a1}7_V#w2Avn6-x^fl7z_60wn)k(^ zO*c}1Vubjs4=KNz`mZn~ZT-k)oK9~BDl15fp0OFF=$h=LJy%S!S%4HK6y5pwRbNcz z7qBL%{+Ns27Z`5UiE+D7Q|5CsN8$tT5GQOKF&m~lTC-8$b1Zq%4e&2rDRF za{jx#`v5YzDakAl4=6h2W{rl;osm}V&75;mGjQ-zt0ZM3`Ivp?Q}z=5R=m^W7;A#j z9mzNh)8T|CNoUN11jO#DZ+Fjz21=F0DxmJ2I@_$X-OC4`W3$7-G!4699N)cL81uGK zd)UrgqwE;R@DK0z@4LJY+9lN@=taqA>rW}REy2T}8&eb=@3*~1(79=R#e4Sw@JZ<` zTNcndfSq3hUZcqvo41F$P1jOhx}%&&(ENmfHT-?E>ehoG)J z5?6p-_zi(Yquk?|@Zbs^e(*TK7#*V#JeWAfgEFdUcN$-F)O*ZQ`X%)!@3)jxZ>j6I zxqA~j2T!mIJq2VOjXG%k9B)3Gym}l*jjl$<8 zBp0%zgVBt2hhf4e3ILunQ?d1h5Zc~tb|_QlQc!iRLAqBi{hZX?~6;9M0Z#tfPvzw^-&n^K}_r4h!NY-jJ`IgiBi#<9ZcUx1p0DJDxjOqwd zzk0?p9312Mm$gaQ;~5?*w6w9RgdC`P2WobPbX`eQop416`nbQ$bAbnwdcYfig0a|K zx*(ucr$!C?#|%2bxuO%al3H?5FRd4K!WajbMo|PQj|31#BFY%y0yf4*m@**dUBRz` z$xOn#9oL733Z!QoW01E3WAG-havQPrF@J^G5|u~66(2eAoX!{v&B1<+=bo~rXh7*O zNcC-1uz`S5HsZxgZ+*N#FS@%N)HO-MHb0nH6zdS7f@U6Gs^Tf7{N!iNCKW*Eq;SR% zJJHF{{<$c$S6~>4K3=1UR7R{+W%VWs{k6q?psSOA7g60)%O@= zVnA~UzjxcA%oCFSUCwLr1}f)?3|6y<2Nd2Dx3G)6XW!AY0)tTXh)FEH17=4K08MoeqWCF0J$TOWaETK-(wY=Jq9TpK^Hf zcF%GOo~EC78@2cIyoNOQD^PdbZgF=Nm(DO3?ziDIwf5T*N|GIe-goTCdFz*b@ws_S zem{yv0&_o2V3lu%f*7|FiS?CS5>$h|aC1x4i$@lM44!qlpOZHPR-GN|j$3Ez#Kb%| zkW7)daCgb>{x;7-g8Uw2I5{1&X+()umbz(O&5TJ%s)b)wSBP)?+Tk3NS-@W*|B5B< z^($#28*)-3RtaymidR%Af9&`^fxv=kFlQ<1^l=ZcK$y{H=!Hpt0;I5hEL8-eC)A;~X3D8bE z<{+D>weik&kYo`?xG@3HMilwHMD{Khj;m7q47+UwzD~@Vx3fibjL(fA!RRAe+2WXV zXEJg!I&=4?7c{122#;t)6N4c*n#Pp~t%P>&JJ)`YFS^1aw~PnRTxG%!`em-Uz|iuP zIJlVwSK&36Dy&;gGuCYXBWXgfa~azzHosPD>6Yei`8u4Aw3Bv1dK z@;%hpcWeU>Fe0LBYN07myXe3teHT`Tqc01HBZ$K(jC=z(^KtNj(-kTToKBB5e+qt? z2$;yRJppT_9~;zQ6kY|RJf6=&1T_y+*+5m@snM#V{84oY)t{KV$8P}>r#RT98tzE3 zw&}rV$)02$hD@{%;~uc3izdlJKm2f6NJJKDriGSr=%QOkJA~Fm<}pkw2Zcv`vY2~@ zVb5icpZlPhy8zBY-CYQzTn=ItZ64KUd-RVy;!t2@9l!^ z`-|DNy)&<40LrHxOJu{QecG7>d!P2thC6Q_EZH-7yvQoz3Wkn!L|Yie-JnbnPTV;$a6Yf(%sQYM{%PP|@qy10#q?X!^{U2( zmI_SH*PNH_)hK__ysSy;%M3g|8Q1BGboWXlYyi}qqU3XrO!a`Kw1I|J#@Jo z7S2P$IK0U)a0%Y{WniGcqbNE&6b|CFJPZQ1J|hBxpu6`#2pGlF3GXDR&uGS3#5*Tl z+aBwW!oUsOIYCIyZl+YB2_FPyDe)WZ-0&WbI7amiQK>LnB!@MyqUJr&H1}}oB>V-v z!fL9j2FG57Hy|>rr&%_##i&gUo8*yX{i4}WyW!%Ymf*R@0uA(UZ5sk&j`hHUB+KoQ z7`izQZaWsVd!h$Bw4l-28!Z;So!)ilxi`Mt zZ|Gkx3ahFLxk*6%D(nU%^n+xAe`~J0u}*@ z^z-AXVg)MKeD*7y9|@L7aJkb-U!$X~YU61g1JzxJH8p}hH{&KK5|6O96IQhV&&Jc7 zJ-o&yr9Th^i=V7cxyvHEkX7}2)#rmqv-m67<51m9phCo!=1RZ%<}i;hOG{afEa{1z zqG`tg!U}q0Q=$gH#j^n*c#M-O!7&A;65K)2rjty^hB`Ln-46@J-`D6_#51XNQ>?NA zb%%ILAsumGL#*XfaF*R>mfK_Iu7@iloAG>v+Pb3{j+|X0RMoWEo5u7f%_bG)EuAC(Ad{FsSo*$ASKEKYFLOhdJD>bcYZ zlNT?Im~Mbwhkt`xN@|X?CC27mp(+e%1dT>Pmsj%&n0E2jKNfQ^nY*;tuVNiJ9nNqm zp%A={>g@T4=XHkZUT#D$9a^~_QUO~nUM}j!SC!E89?FAl_Dy4>UuS9&@JEGx!>ay{ zP!m=9x8{1R73*YaSRS4AhM@XXAqMudi`se>%8rARQDUSV6w9KHsHCCwD3 zxGqo6wkyW2rZL84cHt{^F=qA!*1 zF7Cjrz?zG9-c?H2`MJggx4?)G&_;>TZj~u1n=x{wN`cNX%6$~ffLg2fpmYyKejKTn zrW7@j3(b+TEHW>cM@x2f*&L%o2-VcN<6IkrSfj)vQ|d(5-7cW0CB5C zW~%$>|A7RhzPcup$gZQktj3@lX-HQ6g1;97RLC3) z_CO%{x{ZQTKdyQAGtZV8X^xa#ZjP2jJcBDHiVX8x?#rm~49A%wpy4x{bBWaPHM zh{W`?f~-1IkmG^0h?}H;j&|)<&Az13r?GsmHgje_onikBTdC%z%C2}qvbb%)Qy1{rIAkFGDr zSrqkBU#Y8_rQDaHlw_`6o}Zk`yP`jqo!n_RUh$Kq`N;z71dztgpNrV*#+UtMP_16(+--$DTc+AP-}`> zHqj}*l-_enF(`3CUnP$Ci|E$_Z5SgFW{XL<;Mv(@a>=ZY;)^-zc$POJ0C>rVEN)Dq zIgByDbXDfc3PtOqjXSm92B*_?ORs|$M=fh3X{-cr%flYp>?6GF!hyRo%7G91`9F)_g zhg;x{S_}BZ{Ocr21Pa-rG7`+Q8I)vRAr^C(w+ed}hM6c%538^yH_t2=HS5VtL8-d8 z2~(aJ_Ak2C>5~a7Dvn1P4$fu1I{y(K*UE5a$N-t(5sfgja!(2cXnCg&U@cvx+(lCy z3Y!IBBuH=wA-Dy1hem@tO>lR2X&U#S!65`150>EW?gVSxA-KC+Gx_c;rfTX|&0<#X zA2^%0&Z+0+N}dNY z80d@CBZoR=qxF=-Ld=%SsJWScys2L4IX@`)s_ze{9BTCWN)`?2SpU74mSJnsdLf9f zG>^6E$Zd*{k4eEagTg&Jh*%IGyYzBdYt^Z1#-Q%@dVyUW%o#Y>CJ~*1Gg@}GGJS%z zWP7ZC{3FBCNFI#ks^w6zgS%m%55@fv+4Gto6T|r$`BraySuwHY5i$qAH->!}PC__r zaS)#F;sXL}&<%PU*}grs9DNnpNj@!;%56uPxUE`F>1lT zS^K6U^5w?0!RTyNgISuP9cZU%8UXpw8WJ;SRUwG^j} zbjvs85%md&FM2n3Kf_6l1-~gDF4jSAttEuuVqMPt@)iUIO+KFQ@|#<2!y^`N8KJ^z zQINa{Z)1q*yZi0DNV=}1+J}%$XS&Khx{-a>BLUiL@%kM7;@cGw4mX*r-9>VbCt(-g zO*VYI)o&1IC5-?z2+LXOddt73J3}r(8hC(w$?9`}I>q@9mH^}$uuKe_bj+y?_#p#% zYC0N;A1Qar`Uy$4xjG0YhHS~B=+iSqru*w$U9?o% z`mv6k^A``@mr4^2v>gRjRjC%EqX*wj0l{o_zk8y5UV7jva81;tg>#utaxs+Mlx4_h z4=}9qYQB#YRpu46Mle#wDZf$eN`#^#NB8_tExi2p(|eS#EE+r?6c%!TnLJcfXzpC-MEH85+aoLe6@Fp||CqB`(3R zu;LBMNO3fhQoo_Xp_UCaeLt4m%M&zHZB$?DFCK}f8$71+=;?l!Y6TSZCLeFDvsvax z#Q7XLSIvp(5X09 zcW9Uu24XY{>UrT5)ploSgdzKOKXrp>Jdh2(4Yd0KEC%2JkxtGmH-8cs-rW$hhwyks zxl!fCg++G3i{=VoIp79)h-uhkjvnXyR^KlQO;O+jMQ8D~{xm>wMJJ?(ttJE}rm|-- zkr6pb(^t}B{K1SH&Q)M%<9Q=&pTp`K`w}?dYm+!`xQETh^j(IX^=6wMpU|1HR8Dp+ zb9SR0p1yGKG=o%H6cU!S%*WAqQpDIKF4->7hR?x9v!J%RA8F&OjZ z`?V$3@|Icdrkb*I10p?t|9+y_(-C-Z)GMS+vD!jOi+0AtxZz9fS@h*?Oy1$4Z7NJf zTL-bz@KWpx#h?u!Af5!K?xZ6pRKIttMnFV_eS?$K#$8Ers=o*&m08YVQ33T7q5vSTPSU`B%VOYiYX z-Koy1yG!|u2$flcY3SllcH@dR;f<|w&I=0~TLhHVIUuyull1-+mkE`7J!e8sQs{UC z_TC=#Zi0uX^yv;V0FrN=>Z18_I^OyxdoFH1PIu|fJ%Lme(!jz_K8V4TpE0N4T zNU6D9k^Gwf?ORAWk~}AY~lk0 zGw6AE{?{F`@!T3Tr?0sOT^+4fm{3NkUaZAfeJ|M?n*oG+g!A`;0{Qk=_~Z}Fe45gi zG*oLBdmu4~NNt-~nI)7Tm@31nQq3^&y|F?P{?@1lia42#*aU6K+LAUY_C&U&!;^kU zit9P7`>}kq7xYdgyO^fe>~73lk*jmYBE0blsbFUKG!kUR8Y5CrPavlm3cnUvDqBbg zpWn5Pa;`8SmD`Cuhk3h@wrWzr3^lSKU?#;$`SyWfvOUPcC$;r}wY~RzWl{-HDejKu zToZC^7p_@cV*9(3V}Pter541hI=thG$8##V%C5Z0xW0Ln>5!^q?Y7X>DVtLJU3&2iw5vvmd|$$uFKg~6x9YGs z2WA}Qd%GlxfGs3T34if|F(k)Tu0D$Vc-fQ~Pr{hTDZEo+7|Gg;^qs)j616QIdW&+Z z6uG}ycpscCI#52Ri6~l_F;9d0G?dZ0Nan1wsz08S?G05soJ@(-k(zsafHN(69lljd zaL2CZMN*;l0>zv`fTK-P{6}mJe!I0~oWftO-UV6Z=#f#}qh-bfl zbR{Q+R&u!*eD#*`#zRB1f75?vhxC1SQYDIttu_s;@OGD5XXvYcgPU|t=^W0Vt3Je} z<8K{_x+W?XQIun?dttkSG?z)Dxt79LSl7aItn+-TnUte|SC3^eyL39QxPJ7z8|op5 z5T2>1H7T~gNX~waUeM2A-T!54w* z?ed3QmQm)K421NRSq%?BY(Yt+M=yvxB6xn(>F?rd(KaC%9;W({hw;^Zf)NmX7+D3Tw|8q-0?- zxl$MY0u{qPHkX{cOJEXqbqX?@h+bf?fTi_i3W#vtG5}K;A5jjU-Kn|hU=FM zv9>sM3(CfuUMZPpNc_^{*yP0se?2>-1SU)o0VDC_(0P{}&;$!|q!c+mJx!ew3z)y| ztgnr_?p$1Xh<2>pf4wU21nYvW$%q^wYehMculyP14ln$TdR4I24EbBj=mB#`N4TAXaDh1%&Zx@SqJWIJLGhTo?u*ZXC(6Qb zS*M;nl38y4ew8 zrQrJVc@We*G*g=!B*AgGHc-IMDsS;rh4P3WYBAVhd}m@&~4^}p^|+;dA7DX z=OL%6KL9C#cs@Q3ATt(aq0}(Mj2ptI6hTxm)E1@EP~G(|?Ohx$;4)Y8FTTV$Ktin5 zgo|zVJ2G6;#&R>I_10IGqDz_xbrGRJSGCf7 zccF0a@B&oF5-caJi8y;6Epr1!kc28b@$rWm(|E)f zG|cVj0*+#N!*P#YwEm`6VHdpciLFG8g%Rf;{YLxxcCMDQ^1uI@N*2#H)D<#QB39+M zY4NUaey93d>N{LHPb8w{D6WX4!>5}vC7hzfEFHLFTxzKB(qK1Wc=IfLj)uvC)TvbK zexPvvH>i}vIVT?ZscuqZ^CK>jiK>LD_?IL?ybKjz?)L*Ibws}hen}WobNf%{SCryz z{i(d?Xd;;MrMIQa$LbnL%LkybHY0LwWR4!bJ6(g%1cE}_GY7TmIPIZw$#KY4g;pYAz*Q0~G1W8F;igBL&*fI6(+k)#5-KaDmV^rpYo4 zmt=IHPB5Rw;=^C5uAjQiQ+Kv)Hk0&cBf-%(-DGpUjQwv>-q6qh#DoH>9uHC7L33pJ zmVf1xIQGpU+wB|zerB%s9uBQyXwFF}z?49o`7$<8FZo&cytn@i zoGdS)p#G>JWmkca0fjo&s)u01$w{9uCHRSbhD%OsUnLAMR>gMo3B${RRX3o};%@F$ zUsrkAE!_^gW%pIt(FQtqu3|+mt5l!W$XHw^7Hf^l0Fft}%9d~@BDsez>dkw`@QuOq z-Kp>Qg35)5#QV%>1Ku_^_=l%)o=&__bu#*emHE=q< zzafGvRi5+oT`ezPg*icw>%qRQt@_%_VxZ2ww)$11)dF+{yx@PKl#~KtALY@CmrgSK z=dO-iG)9|;o+yFHRpxYo`V>wtHbIyR9J?=IOX7GYF&L)m7h2aw!{K{%m+6EVuCP>9 zRm6dfp5c6Egc*tr4U#c;E#nKsJA=I>Zo^{ezg&H`9e}x)OE?C+V+2Ghbv0~0p zMYztI$H+>^tn@2YwRLV7yQjp0IPZvUQew@@{7L9`_kLC%$4OO%IGD6#{nuwYN=#R% z$|v%~NitJoTaFR~&o7%0D$dNk8C)eKY9y;DgT0bg0)pKPYafmN2TO9!v~funrwe>QCeI5he`S_C#Qa!_veWX zxRfSU#3odlv=jJd#?NUPy=YNF=O_Z}SQLV)!O(ZM(o}lZbllKm46x1+;|VnJxM$L_ z_w_{Ycst(V3?Xz2KPbdwGDMpB^$|AnQ^te&iS~Mgm0NI#s@j@M32ydXM5eHmM(%f( zzAjW>ANPQ!K_h{OW!uRkJd}iYiXIAKaMe^+Fu6|}5hvBr+(zai8kv#Y^qF>b#7gJk z$W*nGC5PJhR%riAMPg^jM*q?wQSvyHJ)joy*?m4Y`pQ$KA?OFTWlb7;La+<|D2R@j%v;8A?nrU2ZbXSGG{~f`|wtUwN zU}1@boz-^iA@w)u%LU|coA45up2|P@%E^}aX9$wH@56W#=9HKvU}*i-cr+KpCCDx#u8ECDO&z!0< zXT%{Uj!z@0WUlU<$u9t%L(*RVdPnedQ#B$eS+Tn^wY9U40j0Ambvj=#4AsS!j^o|V zdIQURE+uS=jp*{v@>o&JtJOLq0m@~VrWlWN91$FEE6eA(g@0%M!yV-x%*xT&d`%&{ zr{JPlP_|)}_Qca@9FrMgn?^_CUV6x%b#QR@Tb~Ml%WDlLgVL7^5;MaQ9Q4p~xZ%9& zN<`%?Pt}mNBmV`D(mb-`FI(5I)G$ZU!xdIAM=?)d7xlCAJhiQ=uv4;&RMLCDg!qU@ zZ6rVQw5sR@iH~T5&cr|T8LTDC?qHgV^Dcjove%v#PYog+Gw|wQi|ZBGdQSkb7Ljzv zAo}9`NmZ_Bk#Oii}&bmY3K?O+-u89yP)D!hbO=swwEE9>Eh@j6_ z_4da{6{a%wcqklH;8?k}t+5O4`FdFF0Yk3s`*C$(Zf-TWz|a820xGNc9B_K<)`sZO zqKcl@d8owyD?c9T!}TYjnz9-%%*gF#DkCqJqvV|2kumUg^9XJhh%~k@XpI?8b^r?8 z9%hJkK-%Hx=l4DSz7FQIc%a=4s`L7 zer@`xaSj{vwTIklcG$=Mb}BsI!BZ!of?~Q1(Y%M5D3Zkpep6-A7x8@xBrdDNJ#651 zwe)zU{rBl=sh=~zbsnqW#qNZ3W?CeCk#K%c{5kK#nv&K9o8mwq5nuj5DzQKQrMgNs$KN6#L7vuAM88ybuq(O z1siLEJm~qu)N3m4YkPfj@bx-EYSoWd+50v@K>=Y=A%_@WpbGY#aa)n|TVU~L#PlF{ z8YNLmsYx-_;3FO2zD@#66NAA|E!rALkn_n)jlAHw!0*+~sL*sK@0~Pu@JgWOV_=uO u9_efQ$?>buSnMq9)#j*kX<9pH*7`=*U+e#U_9iYK0?!;nwc$_^;QkB0wWTls diff --git a/sorn.egg-info/PKG-INFO b/sorn.egg-info/PKG-INFO deleted file mode 100644 index f4683ab..0000000 --- a/sorn.egg-info/PKG-INFO +++ /dev/null @@ -1,133 +0,0 @@ -Metadata-Version: 2.1 -Name: sorn -Version: 0.6.2 -Summary: Self-Organizing Recurrent Neural Networks -Home-page: https://github.com/Saran-nns/sorn -Author: Saranraj Nambusubramaniyan -Author-email: saran_nns@hotmail.com -License: OSI Approved :: MIT License -Description: - # Self-Organizing Recurrent Neural Networks - - SORN is a class of neuro-inspired artificial network build based on plasticity mechanisms in biological brain and mimic neocortical circuits ability of learning and adaptation. SORN consists of pool of excitatory neurons and small population of inhibitory neurons which are controlled by 5 plasticity mechanisms found in neocortex, namely Spike Timing Dependent Plasticity (STDP), Intrinsic Plasticity (IP), Synaptic Scaling (SS),Synaptic Normalization(SN) and inhibitory Spike Timing Dependent Plasticity (iSTDP). Using mathematical tools, SORN network simplifies the underlying structural and functional connectivity mechanisms responsible for learning and memory in the brain - - 'sorn' is a Python package designed for Self Organizing Recurrent Neural Networks. It provides a research environment for computational neuroscientists to study the self-organization, adaption, learning,memory and behavior of brain circuits by reverse engineering neural plasticity mechanisms. Further to extend the potential applications of `sorn`, a demostrative example of a neuro-robotics experiment using OpenAI gym is also [documented](https://self-organizing-recurrent-neural-networks.readthedocs.io/en/latest/usage.html). - - - [![Build Status](https://github.com/saran-nns/sorn/workflows/Build/badge.svg)](https://github.com/saran-nns/sorn/actions) - [![codecov](https://codecov.io/gh/Saran-nns/sorn/branch/master/graph/badge.svg)](https://codecov.io/gh/Saran-nns/sorn) - [![Documentation Status](https://readthedocs.org/projects/self-organizing-recurrent-neural-networks/badge/?version=latest)](https://self-organizing-recurrent-neural-networks.readthedocs.io/en/latest/?badge=latest) - [![PyPI version](https://badge.fury.io/py/sorn.svg)](https://badge.fury.io/py/sorn) - [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) - [![Downloads](https://pepy.tech/badge/sorn)](https://pepy.tech/project/sorn) - [![DOI](https://zenodo.org/badge/174756058.svg)](https://zenodo.org/badge/latestdoi/174756058) - [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/164AKTA-iCVLq-iR-treLA_Y9keRYrQkH#scrollTo=Rt2YZptMtC14) - [![status](https://joss.theoj.org/papers/7dc447f7a0d17d774b59c8ee15c223c2/status.svg)](https://joss.theoj.org/papers/7dc447f7a0d17d774b59c8ee15c223c2) - -

SORN Reservoir and the evolution of synaptic efficacies

- - - ## Installation - - ```python - pip install sorn - ``` - - The library is still in alpha stage, so you may also want to install the latest version from the development branch - - ```python - pip install git+https://github.com/Saran-nns/sorn - ``` - - ## Dependencies - SORN supports Python 3.5+ ONLY. For older Python versions please use the official Python client. - To install all optional dependencies, - - ```python - pip install 'sorn[all]' - ``` - ## Usage - ### Plasticity Phase - - ```python - import sorn - from sorn import Simulator - import numpy as np - - # Sample input - num_features = 10 - time_steps = 200 - inputs = np.random.rand(num_features,time_steps) - - # Simulate the network with default hyperparameters under gaussian white noise - state_dict, E, I, R, C = Simulator.simulate_sorn(inputs = inputs, phase='plasticity', - matrices=None, noise = True, - time_steps=time_steps) - - ``` - ``` - Network Initialized - Number of connections in Wee 3909 , Wei 1574, Wie 8000 - Shapes Wee (200, 200) Wei (40, 200) Wie (200, 40) - ``` - ### Training Phase - ```python - from sorn import Trainer - # NOTE: During training phase, input to `sorn` should have second (time) dimension set to 1. ie., input shape should be (input_features,1). - - inputs = np.random.rand(num_features,1) - - # SORN network is frozen during training phase - state_dict, E, I, R, C = Trainer.train_sorn(inputs = inputs, phase='training', - matrices=state_dict, noise= False, - time_steps=1, - ne = 100, nu=num_features, - lambda_ee = 10, eta_stdp=0.001 ) - ``` - ### Network Output Descriptions - `state_dict` - Dictionary of connection weights (`Wee`,`Wei`,`Wie`) , Excitatory network activity (`X`), Inhibitory network activities(`Y`), Threshold values (`Te`,`Ti`) - - `E` - Excitatory network activity of entire simulation period - - `I` - Inhibitory network activity of entire simulation period - - `R` - Recurrent network activity of entire simulation period - - `C` - Number of active connections in the Excitatory pool at each time step - - ### Documentation - For detailed documentation about development, analysis, plotting methods and a sample experiment with OpenAI Gym, please visit [SORN Documentation](https://self-organizing-recurrent-neural-networks.readthedocs.io/en/latest/) - - ### Citation - - ```Python - @software{saranraj_nambusubramaniyan_2020_4184103, - author = {Saranraj Nambusubramaniyan}, - title = {Saran-nns/sorn: Stable alpha release}, - month = nov, - year = 2020, - publisher = {Zenodo}, - version = {v0.3.1}, - doi = {10.5281/zenodo.4184103}, - url = {https://doi.org/10.5281/zenodo.4184103} - } - ``` - - ### Contributions - I am welcoming contributions. If you wish to contribute, please create a branch with a pull request and the changes can be discussed there. - If you find a bug in the code or errors in the documentation, please open a new issue in the Github repository and report the bug or the error. Please provide sufficient information for the bug to be reproduced. - - - -Keywords: Brain-Inspired Computing,Artificial Neural Networks,Neuro Informatics,Spiking Cortical Networks,Neural Connectomics,Neuroscience,Artificial General Intelligence,Neural Information Processing -Platform: UNKNOWN -Classifier: Development Status :: 3 - Alpha -Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence -Classifier: License :: OSI Approved :: MIT License -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.5 -Classifier: Programming Language :: Python :: 3.6 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Description-Content-Type: text/markdown diff --git a/sorn.egg-info/SOURCES.txt b/sorn.egg-info/SOURCES.txt deleted file mode 100644 index 5857bdb..0000000 --- a/sorn.egg-info/SOURCES.txt +++ /dev/null @@ -1,13 +0,0 @@ -LICENSE.md -README.md -setup.py -sorn/__init__.py -sorn/sorn.py -sorn/test_sorn.py -sorn/utils.py -sorn.egg-info/PKG-INFO -sorn.egg-info/SOURCES.txt -sorn.egg-info/dependency_links.txt -sorn.egg-info/not-zip-safe -sorn.egg-info/requires.txt -sorn.egg-info/top_level.txt \ No newline at end of file diff --git a/sorn.egg-info/dependency_links.txt b/sorn.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/sorn.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/sorn.egg-info/not-zip-safe b/sorn.egg-info/not-zip-safe deleted file mode 100644 index 8b13789..0000000 --- a/sorn.egg-info/not-zip-safe +++ /dev/null @@ -1 +0,0 @@ - diff --git a/sorn.egg-info/requires.txt b/sorn.egg-info/requires.txt deleted file mode 100644 index f64786e..0000000 --- a/sorn.egg-info/requires.txt +++ /dev/null @@ -1,6 +0,0 @@ -numpy -configparser -scipy -seaborn -pandas -networkx diff --git a/sorn.egg-info/top_level.txt b/sorn.egg-info/top_level.txt deleted file mode 100644 index c5bfb78..0000000 --- a/sorn.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -sorn