Encoding positions of the end-effector using the SNS-Toolbox
There is an example of how to map joint angles into positions of the leg. To get a basic understanding of how to design neural networks using SNS, please go to the:
This example is based on the conference paper: Zadokha, B., Szczecinski, N. S.: Encoding 3D Leg Kinematics using Spatially-Distributed, Population Coded Network Model. (2024)
Initial Setup
The neural network was designed and simulated through PyCharm 2022.
import scipy
import numpy as np
from sns_toolbox.connections import NonSpikingSynapse
import sns_toolbox.networks
import matplotlib.pyplot as plt
from sns_toolbox.neurons import NonSpikingNeuron
import time
import seaborn as sns
widthBell is the width of the sensory-encoding bell curves. Also note that smaller value of widthBell makes the curves broader, delay is a membrane capacitance of the neuron
Create the "bell curve" neurons, which are the sensory (input) neurons. Each sensory neuron has a bell-curve receptive field with a unique "preferred angle", that is, the angle at which the peak response is evoked:
# servo 2
for index in range(numSensPerJoint):
nameStr2 = 'Bell2' + str(index)
net.add_neuron(neuron_type=bellNeur, name=nameStr2)
net.add_input(nameStr2)
net.add_output(nameStr2)
# servo 3
for index in range(numSensPerJoint):
nameStr3 = 'Bell3' + str(index)
net.add_neuron(neuron_type=bellNeur, name=nameStr3)
net.add_input(nameStr3)
net.add_output(nameStr3)
Define key neuron and synapse parameters. 'mag' is like R in the functional subnetwork publications: it is the maximum expected neural activation. delE is the synaptic reversal potential relative to the rest potential of the neurons:
mag = 1
delE = 20
Initialize empty arrays to store input current for each set of sensory neurons (theta2 = input2, theta3 = input3), the input current for the whole network (inputNet), and the validation data X(t) (ActualT):
input2 = np.zeros([numSteps, numSensPerJoint]) # bell curve responses
input3 = np.zeros([numSteps, numSensPerJoint]) # bell curve responses
inputNet = np.zeros([numSteps, numSensPerJoint * numJoints])
# Actual X, Y, Z values as a function of time
ActualT = np.zeros([numSteps, 3])
Each sensory neuron synapses onto a number of "combo" neurons. Each combo neuron receives synaptic input from one sensory neuron from each joint. All these synapses may be identical, because they are simply generating all the possible combinations of the joint angles/independent variables. All the combo neurons may be identical, because they are simply integrating the joint angles:
For the network to perform the desired calculation, the synapses from each combo neuron to the output neuron should have effective gains (i.e., Vout/Vcombo) that are proportional to the value that the output neuron encodes. Here, we take all the "training data", that is, the actual X coordinate of the leg, normalize it to values between 0 and 1, and use those to set the unique properties of the synapse from each combo neuron to the output neuron:
Now we create the each combo neuron, its input connections, and its output connections. Here, we have 2 nested for loops, one for each joint/independent variable. A higher-dimensional network would require more for loops or an approach in which we NDgrid the joint angles/sensory neurons and then use a single for loop:
k = 0
for i in range(numSensPerJoint):
for j in range(numSensPerJoint):
nameStr = 'combo' + str(i) + str(j)
net.add_neuron(neuron_type=comboNeur, name=nameStr)
net.add_connection(identitySyn, source='Bell2' + str(i), destination=nameStr)
net.add_connection(identitySyn, source='Bell3' + str(j), destination=nameStr)
net.add_output(nameStr)
SNS-Toolbox does not enable synapses with max_cond = 0, so if kSyn = 0, we must pass it machine epsilon instead:
Synapses from the combo neurons to the output neuron(s) each have unique conductance value that corresponds to the desired output in that scenario. Note that the e_lo for the synapses = mag and e_hi = 2*mag, because multiple combo neurons will be active at any time, but we only care about the most active, so setting e_lo this way serves as a threshold mechanism:
synOutputX = NonSpikingSynapse(max_conductance=gX, reversal_potential=delE, e_lo=mag, e_hi=2 * mag)
synOutputY = NonSpikingSynapse(max_conductance=gY, reversal_potential=delE, e_lo=mag, e_hi=2 * mag)
synOutputZ = NonSpikingSynapse(max_conductance=gZ, reversal_potential=delE, e_lo=mag, e_hi=2 * mag)
net.add_connection(synOutputX, source=nameStr, destination='OutputEndX')
net.add_connection(synOutputY, source=nameStr, destination='OutputEndY')
net.add_connection(synOutputZ, source=nameStr, destination='OutputEndZ')
# Increment k, which is a linear counting variable through all the for loops.
k += 1
net.add_output('OutputEndX')
net.add_output('OutputEndY')
net.add_output('OutputEndZ')
The input current for the theta2 and theta3 sensory neurons using the bellCurve function:
for i in range(numSensPerJoint):
input2[:, i] = bell_curve(mag, widthBell, theta=servo2vec, shift=jointTheta[i])
input3[:, i] = bell_curve(mag, widthBell, theta=servo3vec, shift=jointTheta[i])
Concatenate the inputs into one network input array. Calculate the "Actual X, Y, Z values as a function of time", ActualT:
for i in range(len(t)):
inputNet[i, :] = np.concatenate((input2[i, :], input3[i, :]), axis=None)
p1TrnslMat = FrwrdKnmtcsFunc(wR1, np.array([0, theta2vec[i], theta3vec[i]]), pR1)
ActualT[i, 0] = p1TrnslMat[0, 3] # x coord
ActualT[i, 1] = p1TrnslMat[1, 3] # y coord
ActualT[i, 2] = p1TrnslMat[2, 3] # z coord