Source code for superneuromat

import numpy as np
import pandas as pd
import ctypes
import os


"""

TODO:

1. create_neurons() 
2. create_synapses() 
3. Tutorials: one neuron, two neurons 
4. Efficient STDP algorithm without loop

"""



"""

FEATURE REQUESTS: 

1. Visualize spike raster
2. Monitor STDP synapses
3. Reset neuromorphic model
4. Count number of spikes

"""




[docs] class NeuromorphicModel: """ Defines a neuromorphic model with neurons and synapses Attributes: num_neurons (int): Number of neurons in the neuromorphic model neuron_thresholds (list): List of neuron thresholds neuron_leaks (list): List of neuron leaks, defined as the amount by which the internal states of the neurons are pushed towards the neurons' reset states neuron_reset_states (list): List of neuron reset states neuron_refractory_periods (list): List of neuron refractory periods num_synapses (int): Number of synapses in the neuromorphic model pre_synaptic_neuron_ids (list): List of pre-synaptic neuron IDs post_synaptic_neuron_ids (list): List of post-synaptic neuron IDs synaptic_weights (list): List of synaptic weights synaptic_delays (list): List of synaptic delays enable_stdp (list): List of Boolean values denoting whether STDP learning is enabled on each synapse input_spikes (dict): Dictionary of input spikes indexed by time spike_train (list): List of spike trains for each time step stdp (bool): Boolean parameter that denotes whether STDP learning has been enabled in the neuromorphic model stdp_time_steps (int): Number of time steps over which STDP updates are made stdp_Apos (list): List of STDP parameters per time step for excitatory update of weights stdp_Aneg (list): List of STDP parameters per time step for inhibitory update of weights Methods: create_neuron: Creates a neuron in the neuromorphic model create_synapse: Creates a synapse in the neuromorphic model add_spike: Add an external spike at a particular time step for a given neuron with a given value stdp_setup: Setup the STDP parameters setup: Setup the neuromorphic model and prepare for simulation simulate: Simulate the neuromorphic model for a given number of time steps print_spike_train: Print the spike train CAUTION: 1. Delay is implemented by adding a chain of proxy neurons. A delay of 10 between neuron A and neuron B would add 9 proxy neurons between A and B. 2. Leak brings the internal state of the neuron back to the reset state. The leak value is the amount by which the internal state of the neuron is pushed towards its reset state. 3. Deletion of neurons is not permitted 4. Input spikes can have a value 5. All neurons are monitored by default """ def __init__(self, backend="cpu"): """ Initialize the neuromorphic model Args: backend (string): Backend is either 'cpu' or 'frontier' Raises: TypeError if: 1. backend is not a string ValueError if: 2. backend is not one of the following values: cpu, frontier """ # Type errors if not isinstance(backend, str): raise TypeError("backend must be either 'cpu', or 'frontier'") # Value errors if backend not in {"cpu", "frontier"}: raise ValueError("backend must be either 'cpu', or 'frontier'") # Neuron parameters self.num_neurons = 0 self.neuron_thresholds = [] self.neuron_leaks = [] self.neuron_reset_states = [] self.neuron_refractory_periods = [] # Synapse parameters self.num_synapses = 0 self.pre_synaptic_neuron_ids = [] self.post_synaptic_neuron_ids = [] self.synaptic_weights = [] self.synaptic_delays = [] self.enable_stdp = [] # Input spikes (can have a value) self.input_spikes = {} # Spike trains (monitoring all neurons) self.spike_train = [] # STDP Parameters self.stdp = False self.stdp_time_steps = 0 self.stdp_Apos = [] self.stdp_Aneg = [] self.stdp_positive_update = False self.stdp_negative_update = False # Hardware backend parameters self.backend = backend print(f"[Python Source] Backend is {backend}") if backend == "frontier": print("This functionality is a work in progress...") # print(os.system("pwd")) # # Compile frontier.c program as a shared library # self.c_file_path = "../superneuromat/frontier.c" # self.executable_name = "shared_library_frontier.so" # self.compile_command = f"gcc -shared -o {self.executable_name} {self.c_file_path}" # print("[Python Source] Compiling C program") # status = os.system(self.compile_command) # if status == 0: # print("[Python Source] Successfully compiled C program") # else: # print("[Python Source] C program compilation unsuccessful") # # Load the shared library # self.c_frontier_library = ctypes.CDLL("./shared_library_frontier.so") def __repr__(self): """ Display the neuromorphic class in a legible format """ num_neurons_str = f"Number of neurons: {self.num_neurons}" num_synapses_str = f"Number of synapses: {self.num_synapses}" # Neurons neuron_df = pd.DataFrame({ "Neuron ID": list(range(self.num_neurons)), "Threshold": self.neuron_thresholds, "Leak": self.neuron_leaks, "Reset State": self.neuron_reset_states, "Refractory Period": self.neuron_refractory_periods }) # Synapses synapse_df = pd.DataFrame({ "Synapse ID": list(range(self.num_synapses)), "Pre Neuron ID": self.pre_synaptic_neuron_ids, "Post Neuron ID": self.post_synaptic_neuron_ids, "Weight": self.synaptic_weights, "Delay": self.synaptic_delays, "STDP Enabled": self.enable_stdp }) # STDP stdp_info = f"STDP Enabled: {self.stdp} \n" + \ f"STDP Time Steps: {self.stdp_time_steps} \n" + \ f"STDP A positive: {self.stdp_Apos} \n" + \ f"STDP A negative: {self.stdp_Aneg}" # Input Spikes times = [] nids = [] values = [] for time in self.input_spikes: for nid, value in zip(self.input_spikes[time]["nids"], self.input_spikes[time]["values"]): times.append(time) nids.append(nid) values.append(value) input_spikes_df = pd.DataFrame( { "Time": times, "Neuron ID": nids, "Value": values }) # Spike train spike_train = "" for time, spikes in enumerate(self.spike_train): spike_train += f"Time: {time}, Spikes: {spikes}\n" return num_neurons_str + "\n" + \ num_synapses_str + "\n" \ "\nNeuron Info: \n" + \ neuron_df.to_string(index=False) + "\n" + \ "\nSynapse Info: \n" + \ synapse_df.to_string(index=False) + "\n" + \ "\nSTDP Info: \n" + \ stdp_info + "\n" + \ "\nInput Spikes: \n" + \ input_spikes_df.to_string(index=False) + "\n" + \ "\nSpike Train: \n" + \ spike_train
[docs] def create_neuron( self, threshold: float=0.0, leak: float=np.inf, reset_state: float=0.0, refractory_period: int=0 ) -> int: """ Create a neuron Args: threshold (float): Neuron threshold; the neuron spikes if its internal state is strictly greater than the neuron threshold (default: 0.0) leak (float): Neuron leak; the amount by which by which the internal state of the neuron is pushed towards its reset state (default: np.inf) reset_state (float): Reset state of the neuron; the value assigned to the internal state of the neuron after spiking (default: 0.0) refractory_period (int): Refractory period of the neuron; the number of time steps for which the neuron remains in a dormant state after spiking Returns: Returns the neuron ID Raises: TypeError if: 1. threshold is not an int or a float 2. leak is not an int or a float 3. reset_state is not an int or a float 4. refractory_period is not an int ValueError if: 1. leak is less than 0.0 2. refractory_period is less than 0 """ # Type errors if not isinstance(threshold, (int, float)) : raise TypeError("threshold must be int or float") if not isinstance(leak, (int, float)): raise TypeError("leak must be int or float") if not isinstance(reset_state, (int, float)): raise TypeError("reset_state must be int or float") if not isinstance(refractory_period, int): raise TypeError("refractory_period must be int") # Value errors if leak < 0.0: raise ValueError("leak must be grater than or equal to zero") if refractory_period < 0: raise ValueError("refractory_period must be greater than or equal to zero") # Collect neuron parameters self.neuron_thresholds.append(threshold) self.neuron_leaks.append(leak) self.neuron_reset_states.append(reset_state) self.neuron_refractory_periods.append(refractory_period) self.num_neurons += 1 # Return neuron ID return self.num_neurons - 1
[docs] def create_synapse( self, pre_id: int, post_id: int, weight: float = 1.0, delay: int=1, stdp_enabled: bool=False ) -> None: """ Creates a synapse in the neuromorphic model from a pre-synaptic neuron to a post-synaptic neuron with a given set of synaptic parameters (weight, delay and enable_stdp) Args: pre_id (int): ID of the pre-synaptic neuron post_id (int): ID of the post-synaptic neuron weight (float): Synaptic weight; weight is multiplied to the incoming spike (default: 1.0) delay (int): Synaptic delay; number of time steps by which the outgoing signal of the syanpse is delayed by (default: 1) enable_stdp (bool): Boolean value that denotes whether or not STDP learning is enabled on the synapse (default: False) Raises: TypeError if: 1. pre_id is not an int 2. post_id is not an int 3. weight is not a float 4. delay is not an int 5. enable_stdp is not a bool ValueError if: 1. pre_id is less than 0 2. post_id is less than 0 3. delay is less than or equal to 0 """ # Type errors if not isinstance(pre_id, int): raise TypeError("pre_id must be int") if not isinstance(post_id, int): raise TypeError("post_id must be int") if not isinstance(weight, (int, float)): raise TypeError("weight must be a float") if not isinstance(delay, int): raise TypeError("delay must be an integer") if not isinstance(stdp_enabled, bool): raise TypeError("enable_stdp must be a bool") # Value errors if pre_id < 0: raise ValueError("pre_id must be greater than or equal to zero") if post_id < 0: raise ValueError("post_id must be greater than or equal to zero") if delay <= 0: raise ValueError("delay must be greater than or equal to 1") # Collect synapse parameters if delay == 1: self.pre_synaptic_neuron_ids.append(pre_id) self.post_synaptic_neuron_ids.append(post_id) self.synaptic_weights.append(weight) self.synaptic_delays.append(delay) self.enable_stdp.append(stdp_enabled) self.num_synapses += 1 else: for d in range(int(delay) - 1): temp_id = self.create_neuron() self.create_synapse(pre_id, temp_id) pre_id = temp_id self.create_synapse(pre_id, post_id, weight=weight, stdp_enabled=stdp_enabled) # Return synapse ID return self.num_synapses - 1
[docs] def add_spike( self, time: int, neuron_id: int, value: float=1.0 ) -> None: """ Adds an external spike in the neuromorphic model Args: time (int): The time step at which the external spike is added neuron_id (int): The neuron for which the external spike is added value (float): The value of the external spike (default: 1.0) Raises: TypeError if: 1. time is not an int 2. neuron_id is not an int 3. value is not an int or float """ # Type errors if not isinstance(time, int): raise TypeError("time must be int") if not isinstance(neuron_id, int): raise TypeError("neuron_id must be int") if not isinstance(value, (int, float)): raise TypeError("value must be int or float") # Value errors if time < 0: raise ValueError("time must be greater than or equal to zero") if neuron_id < 0: raise ValueError("neuron_id must be greater than or equal to zero") # Add spikes if time in self.input_spikes: self.input_spikes[time]["nids"].append(neuron_id) self.input_spikes[time]["values"].append(value) else: self.input_spikes[time] = {} self.input_spikes[time]["nids"] = [neuron_id] self.input_spikes[time]["values"] = [value]
[docs] def stdp_setup( self, time_steps: int=3, Apos: list=[1.0, 0.5, 0.25], Aneg: list=[1.0, 0.5, 0.25], positive_update: bool=True, negative_update: bool=True ) -> None: """ Setup the Spike-Time-Dependent Plasticity (STDP) parameters Args: time_steps (int): Number of time steps over which STDP learning occurs (default: 3) Apos (list): List of parameters for excitatory STDP updates (default: [1.0, 0.5, 0.25]); number of elements in the list must be equal to time_steps Aneg (list): List of parameters for inhibitory STDP updates (default: [1.0, 0.5, 0.25]); number of elements in the list must be equal to time_steps positive_update (bool): Boolean parameter indicating whether excitatory STDP update should be enabled negative_update (bool): Boolean parameter indicating whether inhibitory STDP update should be enabled Raises: TypeError if: 1. time_steps is not an int 2. Apos is not a list 3. Aneg is not a list 4. positive_update is not a bool 5. negative_update is not a bool ValueError if: 1. time_steps is less than or equal to zero 2. Number of elements in Apos is not equal to the time_steps 3. Number of elements in Aneg is not equal to the time_steps 4. The elements of Apos are not int or float 5. The elements of Aneg are not int or float 6. The elements of Apos are not greater than or equal to 0.0 7. The elements of Apos are not greater than or equal to 0.0 RuntimeError if: 1. enable_stdp is not set to True on any of the synapses """ # Type errors if not isinstance(time_steps, int): raise TypeError("time_steps should be int") if not isinstance(Apos, list): raise TypeError("Apos should be a list") if not isinstance(Aneg, list): raise TypeError("Aneg should be a list") if not isinstance(positive_update, bool): raise TypeError("positive_update must be a bool") if not isinstance(negative_update, bool): raise TypeError("negative_update must be a bool") # Value error if time_steps <= 0: raise ValueError("time_steps should be greater than zero") if positive_update and len(Apos) != time_steps: raise ValueError(f"Length of Apos should be {time_steps}") if negative_update and len(Aneg) != time_steps: raise ValueError(f"Length of Aneg should be {time_steps}") if positive_update and not all([isinstance(x, (int, float)) for x in Apos]): raise ValueError("All elements in Apos should be int or float") if negative_update and not all([isinstance(x, (int, float)) for x in Aneg]): raise ValueError("All elements in Aneg should be int or float") if positive_update and not all([x >= 0.0 for x in Apos]): raise ValueError("All elements in Apos should be positive") if negative_update and not all([x >= 0.0 for x in Aneg]): raise ValueError("All elements in Aneg should be positive") # Runtime error if not any(self.enable_stdp): raise RuntimeError("STDP is not enabled on any synapse, might want to skip stdp_setup()") # Collect STDP parameters self.stdp = True self.stdp_time_steps = time_steps self.stdp_Apos = Apos self.stdp_Aneg = Aneg self.stdp_positive_update = positive_update self.stdp_negative_update = negative_update
[docs] def setup(self): """ Setup the neuromorphic model for simulation """ # Create numpy arrays for neuron state variables self._neuron_thresholds = np.array(self.neuron_thresholds) self._neuron_leaks = np.array(self.neuron_leaks) self._neuron_reset_states = np.array(self.neuron_reset_states) self._neuron_refractory_periods_original = np.array(self.neuron_refractory_periods) self._neuron_refractory_periods = np.zeros(self.num_neurons) self._internal_states = np.array(self.neuron_reset_states) self._spikes = np.zeros(self.num_neurons) # Create numpy arrays for synapse state variables self._weights = np.zeros((self.num_neurons, self.num_neurons)) self._weights[self.pre_synaptic_neuron_ids, self.post_synaptic_neuron_ids] = self.synaptic_weights # Create numpy arrays for STDP state variables if self.stdp: self._stdp_enabled_synapses = np.zeros((self.num_neurons, self.num_neurons)) self._stdp_enabled_synapses[self.pre_synaptic_neuron_ids, self.post_synaptic_neuron_ids] = self.enable_stdp # Create numpy array for input spikes state variable self._input_spikes = np.zeros(self.num_neurons)
[docs] def simulate(self, time_steps: int=1000) -> None: """ Simulate the neuromorphic spiking neural network Args: time_steps (int): Number of time steps for which the neuromorphic circuit is to be simulated backend (string): Backend is either cpu or frontier Raises: TypeError if: 1. time_steps is not an int 2. backend is not a string ValueError if: 1. time_steps is less than or equal to zero 2. backend is not one of the following values: cpu, frontier """ # Type errors if not isinstance(time_steps, int): raise TypeError("time_steps must be int") # Value errors if time_steps <= 0: raise ValueError("time_steps must be greater than zero") # Select appropriate simulation function if self.backend == "cpu": self._simulate_cpu(time_steps=time_steps) elif self.backend == "frontier": self._simulate_frontier(time_steps=time_steps) else: raise RuntimeError("Unsupported backend:", backend)
def _simulate_cpu(self, time_steps: int=1000): """ Simulates the neuromorphic SNN on CPUs """ # Simulate for time_step in range(time_steps): # Leak: internal state > reset state indices = (self._internal_states > self._neuron_reset_states) self._internal_states[indices] = np.maximum(self._internal_states[indices] - self._neuron_leaks[indices], self._neuron_reset_states[indices]) # Leak: internal state < reset state indices = (self._internal_states < self._neuron_reset_states) self._internal_states[indices] = np.minimum(self._internal_states[indices] + self._neuron_leaks[indices], self._neuron_reset_states[indices]) # Zero out _input_spikes to prepare them for input spikes in current time step self._input_spikes -= self._input_spikes # Include input spikes for current time step if time_step in self.input_spikes: self._input_spikes[self.input_spikes[time_step]["nids"]] = self.input_spikes[time_step]["values"] # Internal state self._internal_states = self._internal_states + self._input_spikes + (self._weights.T @ self._spikes) # Compute spikes self._spikes = np.greater(self._internal_states, self._neuron_thresholds).astype(int) # Refractory period: Compute indices of neuron which are in their refractory period indices = np.greater(self._neuron_refractory_periods, 0) # For neurons in their refractory period, zero out their spikes and decrement refractory period by one self._spikes[indices] = 0 self._neuron_refractory_periods[indices] -= 1 # For spiking neurons, turn on refractory period self._neuron_refractory_periods[self._spikes.astype(bool)] = self._neuron_refractory_periods_original[self._spikes.astype(bool)] # Reset internal states self._internal_states[self._spikes == 1.0] = self._neuron_reset_states[self._spikes == 1.0] # Append spike train self.spike_train.append(self._spikes) # STDP Operations t = min(self.stdp_time_steps, len(self.spike_train)-1) if t > 0: _update_synapses = np.outer(np.array(self.spike_train[-t-1:-1]), np.array(self.spike_train[-1])).reshape([-1, self.num_neurons, self.num_neurons]) if self.stdp_positive_update: self._weights += ((_update_synapses.T * self.stdp_Apos[0:t][::-1]).T).sum(axis=0) * self._stdp_enabled_synapses if self.stdp_negative_update: self._weights += (((1 - _update_synapses).T * self.stdp_Aneg[0:t][::-1]).T).sum(axis=0) * self._stdp_enabled_synapses # Update weights if STDP was enabled if self.stdp: self.synaptic_weights = list(self._weights[self.pre_synaptic_neuron_ids, self.post_synaptic_neuron_ids]) def _simulate_frontier(self, time_steps): """ Simulates the neuromorphic SNN on the Frontier supercomputer """ # Define argument and return types self.c_frontier_library.argtypes = [ctypes.c_int] self.c_frontier_library.restype = ctypes.c_int # Call the C function data = 10 result = self.c_frontier_library.simulate_frontier(data) print(f"[Python Source] Result from C: {result}")
[docs] def print_spike_train(self): """ Prints the spike train """ for time, spike_train in enumerate(self.spike_train): print(f"Time: {time}, Spikes: {spike_train}")