From a5987e1db3a16884b27dc15f96f86f5ef7d3deb5 Mon Sep 17 00:00:00 2001 From: "Risbud, Sumedh" Date: Wed, 7 Sep 2022 13:37:34 -0700 Subject: [PATCH 1/5] SCIF neuron model for QUBO, CSP, etc. Signed-off-by: Risbud, Sumedh --- src/lava/proc/scif/models.py | 130 ++++++++++++++ src/lava/proc/scif/process.py | 58 ++++++ tests/lava/proc/scif/__init__.py | 0 tests/lava/proc/scif/test_models.py | 257 +++++++++++++++++++++++++++ tests/lava/proc/scif/test_process.py | 34 ++++ 5 files changed, 479 insertions(+) create mode 100644 src/lava/proc/scif/models.py create mode 100644 src/lava/proc/scif/process.py create mode 100644 tests/lava/proc/scif/__init__.py create mode 100644 tests/lava/proc/scif/test_models.py create mode 100644 tests/lava/proc/scif/test_process.py diff --git a/src/lava/proc/scif/models.py b/src/lava/proc/scif/models.py new file mode 100644 index 000000000..7b26b3ac8 --- /dev/null +++ b/src/lava/proc/scif/models.py @@ -0,0 +1,130 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# See: https://spdx.org/licenses/ + +import numpy as np + +from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol +from lava.magma.core.model.py.ports import PyInPort, PyOutPort +from lava.magma.core.model.py.type import LavaPyType +from lava.magma.core.resources import CPU +from lava.magma.core.decorator import implements, requires, tag +from lava.magma.core.model.py.model import PyLoihiProcessModel +from lava.magma.core.model.lfsr_model import adv_lfsr_nbits +from lava.proc.scif.process import SCIF + + +@implements(proc=SCIF, protocol=LoihiProtocol) +@requires(CPU) +@tag('fixed_pt') +class PySCIFModelFixed(PyLoihiProcessModel): + """Fixed point implementation of Stochastic Constraint Integrate and + Fire (SCIF) neuron. + """ + a_in = LavaPyType(PyInPort.VEC_DENSE, int, precision=8) + s_sig_out = LavaPyType(PyOutPort.VEC_DENSE, int, precision=8) + s_wta_out = LavaPyType(PyOutPort.VEC_DENSE, int, precision=8) + + u: np.ndarray = LavaPyType(np.ndarray, int, precision=24) + v: np.ndarray = LavaPyType(np.ndarray, int, precision=24) + beta: np.ndarray = LavaPyType(np.ndarray, int, precision=8) + + bias: np.ndarray = LavaPyType(np.ndarray, int, precision=24) + theta: np.ndarray = LavaPyType(np.ndarray, int, precision=24) + neg_tau_ref: np.ndarray = LavaPyType(np.ndarray, int, precision=24) + enable_noise: np.ndarray = LavaPyType(np.ndarray, int, precision=1) + + def run_spk(self) -> None: + # Receive synaptic input + a_in_data = self.a_in.recv() + + # Define spike vectors + s_sig = np.zeros_like(self.v) + s_wta = np.zeros_like(self.v) + + # Saturated add the incoming activation + self.u += a_in_data + self.u[self.u > 2 ** 23 - 1] = 2 ** 23 - 1 + self.u[self.u < -2 ** 23] = -2 ** 23 + + # Populate the buffer for local computation + lsb = self.beta.copy() + lsb &= 3 + self.beta <<= 2 + self.beta[self.beta >= 256] = 0 # Overflow 8-bit unsigned beta to 0 + + # Gather spike and unsatisfied indices for summation axons + sig_unsat_idx = np.where(lsb == 2) + sig_spk_idx = np.where(np.logical_and(lsb == 1, self.u == 0)) + + # First set of unsatisfied WTA indices based on beta and u + wta_unsat_idx = np.where(np.logical_and(lsb == 1, self.u < 0)) + + # Reset voltages of unsatisfied WTA + self.v[wta_unsat_idx] = 0 + + # Assign sigma spikes (+/- 1) + s_sig[sig_unsat_idx] = -1 + s_sig[sig_spk_idx] = 1 + + # Determine neurons under refractory and not refractory + rfct_idx = np.where(self.v < 0) # indices of neurons in refractory + not_rfct_idx = np.where(self.v >= 0) # neurons not in refractory + + # Split/fork state variables u, v, beta + v_in_rfct = self.v[rfct_idx] # voltages in refractory + u_in_rfct = self.u[rfct_idx] # currents in refractory + beta_in_rfct = self.beta[rfct_idx] # beta in refractory + v_to_intg = self.v[not_rfct_idx] # voltages to be integrated + u_to_intg = self.u[not_rfct_idx] # currents to be integrated + beta_to_intg = self.beta[not_rfct_idx] # beta to be integrated + bias_to_intg = self.bias[not_rfct_idx] # bias to be integrated + + # Integration of constraints + # ToDo: Choosing a 16-bit signed random integer. For bit-accuracy, + # need to replace it with Loihi-conformant LFSR function + # If noise is enabled, choose a 16-bit signed random integer, + # else choose zeros + lfsr = np.zeros_like(v_to_intg) + if lfsr.size > 0: + rand_nums = \ + np.random.randint(-2 ** 15, 2 ** 15 - 1, + size=np.count_nonzero(self.enable_noise == 1)) + lfsr[self.enable_noise == 1] = rand_nums + + lfsr = np.right_shift(lfsr, 1) + v_to_intg = v_to_intg + lfsr + u_to_intg + bias_to_intg + v_to_intg[v_to_intg > 2 ** 23 - 1] = 2 ** 23 - 1 # Saturate at max + v_to_intg[v_to_intg < 0] = 0 # Remove negatives + + # WTA spike indices when threshold is exceeded + wta_spk_idx = np.where(v_to_intg >= self.theta) # Exceeds threshold + + # Spiking neuron voltages go in refractory + v_to_intg[wta_spk_idx] = self.neg_tau_ref # Post spk refractory + beta_to_intg[wta_spk_idx] |= 1 + s_wta[wta_spk_idx] = 1 # issue +1 WTA spikes + + # Refractory dynamics + v_in_rfct += 1 # voltage increments by 1 every step + beta_in_rfct |= 3 + # Second set of unsatisfied WTA indices based on v and u in refractory + wta_unsat_idx_2 = np.where(np.logical_or(v_in_rfct == 0, u_in_rfct < 0)) + + # Reset voltage of unsatisfied WTA in refractory + v_in_rfct[wta_unsat_idx_2] = 0 + beta_in_rfct[wta_unsat_idx_2] &= 2 + s_wta[wta_unsat_idx] = -1 + s_wta[wta_unsat_idx_2] = -1 + + # Assign all temporary states to state Vars + self.v[rfct_idx] = v_in_rfct + self.v[not_rfct_idx] = v_to_intg + self.u[rfct_idx] = u_in_rfct + self.u[not_rfct_idx] = u_to_intg + self.beta[rfct_idx] = beta_in_rfct + self.beta[not_rfct_idx] = beta_to_intg + + # Send out spikes + self.s_sig_out.send(s_sig) + self.s_wta_out.send(s_wta) diff --git a/src/lava/proc/scif/process.py b/src/lava/proc/scif/process.py new file mode 100644 index 000000000..94b55243c --- /dev/null +++ b/src/lava/proc/scif/process.py @@ -0,0 +1,58 @@ +# Copyright (C) 2021-22 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# See: https://spdx.org/licenses/ + +import typing as ty + +import numpy as np + +from lava.magma.core.process.process import AbstractProcess +from lava.magma.core.process.variable import Var +from lava.magma.core.process.ports.ports import InPort, OutPort + + +class SCIF(AbstractProcess): + + def __init__( + self, + *, + shape: ty.Tuple[int, ...], + bias: ty.Optional[int] = 1, + theta: ty.Optional[int] = 4, + neg_tau_ref: ty.Optional[int] = -5) -> None: + """ + Stochastic Constraint Integrate and Fire neuron Process. + + Parameters + ---------- + shape: Tuple + shape of the sigma process. Default is (1,). + bias: int + bias current driving the SCIF neuron. Default is 1 (arbitrary). + theta: int + threshold above which a SCIF neuron would fire winner-take-all + spike. Default is 4 (arbitrary). + neg_tau_ref: int + refractory time period (in number of algorithmic time-steps) for a + SCIF neuron after firing winner-take-all spike. Default is -5 ( + arbitrary). + """ + super().__init__(shape=shape) + + self.a_in = InPort(shape=shape) + self.s_sig_out = OutPort(shape=shape) + self.s_wta_out = OutPort(shape=shape) + + self.u = Var(shape=shape, init=np.zeros(shape=shape).astype(int)) + self.v = Var(shape=shape, init=np.zeros(shape=shape).astype(int)) + self.beta = Var(shape=shape, init=np.zeros(shape=shape).astype(int)) + self.enable_noise = Var(shape=shape, init=np.zeros( + shape=shape).astype(int)) + + self.bias = Var(shape=shape, init=int(bias)) + self.theta = Var(shape=(1,), init=int(theta)) + self.neg_tau_ref = Var(shape=(1,), init=int(neg_tau_ref)) + + @property + def shape(self) -> ty.Tuple[int, ...]: + return self.proc_params['shape'] diff --git a/tests/lava/proc/scif/__init__.py b/tests/lava/proc/scif/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/lava/proc/scif/test_models.py b/tests/lava/proc/scif/test_models.py new file mode 100644 index 000000000..2876ba1f7 --- /dev/null +++ b/tests/lava/proc/scif/test_models.py @@ -0,0 +1,257 @@ + +# INTEL CORPORATION CONFIDENTIAL AND PROPRIETARY +# +# Copyright © 2021-2022 Intel Corporation. +# +# This software and the related documents are Intel copyrighted +# materials, and your use of them is governed by the express +# license under which they were provided to you (License). Unless +# the License provides otherwise, you may not use, modify, copy, +# publish, distribute, disclose or transmit this software or the +# related documents without Intel's prior written permission. +# +# This software and the related documents are provided as is, with +# no express or implied warranties, other than those that are +# expressly stated in the License. +import sys +import unittest +import numpy as np +from typing import Tuple, Dict + +from lava.magma.core.run_configs import Loihi2SimCfg +from lava.magma.core.run_conditions import RunSteps +from lava.proc.scif.process import SCIF +from lava.proc.lif.process import LIF +from lava.proc.dense.process import Dense +from lava.proc.io.source import RingBuffer as SpikeSource + +verbose = True if (('-v' in sys.argv) or ('--verbose' in sys.argv)) else False + + +class TestSCIFModels(unittest.TestCase): + """Tests for sigma delta neuron""" + + def run_test( + self, + num_steps: int, + num_neurons: int, + bias: int, + theta: int, + neg_tau_ref: int, + wt: int, + t_inj_spk: Dict[int, int], # time_step -> payload dict to inject + tag: str = 'fixed_pt' + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + + spk_src = SpikeSource(data=np.array([[0] * num_neurons]).reshape( + num_neurons, 1).astype(int)) + dense_in = Dense(weights=(-1) * np.eye(num_neurons), + num_message_bits=16) + scif = SCIF(shape=(num_neurons,), + bias=bias, + theta=theta, + neg_tau_ref=neg_tau_ref) + dense_wta = Dense(weights=wt * np.eye(num_neurons), + num_message_bits=16) + dense_sig = Dense(weights=wt * np.eye(num_neurons), + num_message_bits=16) + lif_wta = LIF(shape=(num_neurons,), + du=4095, + dv=4096, + bias_mant=0, + vth=2**17-1) + lif_sig = LIF(shape=(num_neurons,), + du=4095, + dv=4096, + bias_mant=0, + vth=2**17-1) + spk_src.s_out.connect(dense_in.s_in) + dense_in.a_out.connect(scif.a_in) + scif.s_wta_out.connect(dense_wta.s_in) + scif.s_sig_out.connect(dense_sig.s_in) + dense_wta.a_out.connect(lif_wta.a_in) + dense_sig.a_out.connect(lif_sig.a_in) + + run_condition = RunSteps(num_steps=1) + run_config = Loihi2SimCfg(select_tag=tag) + + volts_scif = [] + volts_lif_wta = [] + volts_lif_sig = [] + for j in range(num_steps): + if j + 1 in t_inj_spk: + spk_src.data.set(np.array([[t_inj_spk[j + 1]] * + num_neurons]).astype(int)) + scif.run(condition=run_condition, run_cfg=run_config) + spk_src.data.set(np.array([[0] * num_neurons]).astype(int)) + volts_scif.append(scif.v.get()) + # Get the voltage of LIF attached to WTA + v_wta = lif_wta.v.get() + # Transform the voltage into +/- 1 spike + v_wta = (v_wta / wt).astype(int) # De-scale the weight + v_wta = np.right_shift(v_wta, 6) # downshift DendAccum's effect + # Append to list + volts_lif_wta.append(v_wta) + # Get the voltage of LIF attached to Sig + v_sig = lif_sig.v.get() + # Transform the voltage into +/- 1 spike + v_sig = (v_sig / wt).astype(int) # De-scale the weight + v_sig = np.right_shift(v_sig, 6) # downshift DendAccum's effect + # Append to list + volts_lif_sig.append(v_sig) + + scif.stop() + + return np.array(volts_scif).astype(int), \ + np.array(volts_lif_wta).astype(int), \ + np.array(volts_lif_sig).astype(int) + + def test_scif_fixed_pt_no_noise(self) -> None: + """Test a single SCIF neuron without noise, but with a constant bias. + The neuron is expected to spike with a regular period, on WTA as well as + Sigma axons. After excitatory spikes on two consecutive time-steps, the + neuron goes into inhibition and sends 2 inhibitory spikes of payload -1 + at the end of its refractory period. + """ + num_neurons = np.random.randint(1, 11) + bias = 1 + theta = 4 + neg_tau_ref = -5 + wt = 2 + total_period = theta // bias - neg_tau_ref + num_epochs = 10 + num_steps = num_epochs * total_period + (theta // bias) + v_scif, v_lif_wta, v_lif_sig = self.run_test(num_steps=num_steps, + num_neurons=num_neurons, + bias=bias, + theta=theta, + neg_tau_ref=neg_tau_ref, + wt=wt, + t_inj_spk={}) + # voltages = np.hstack((np.arange(num_steps).reshape(num_steps, 1), + # v_scif, + # v_lif_wta, + # v_lif_sig)) + # print(voltages) + spk_idxs = np.array([theta // bias - 1 + j * total_period for j in + range(num_epochs)]).astype(int) + wta_pos_spk_idxs = spk_idxs + 1 + sig_pos_spk_idxs = wta_pos_spk_idxs + 1 + wta_neg_spk_idxs = wta_pos_spk_idxs - neg_tau_ref + sig_neg_spk_idxs = sig_pos_spk_idxs - neg_tau_ref + self.assertTrue(np.all(v_scif[spk_idxs] == neg_tau_ref)) + self.assertTrue(np.all(v_lif_wta[wta_pos_spk_idxs] == 1)) + self.assertTrue(np.all(v_lif_sig[sig_pos_spk_idxs] == 1)) + self.assertTrue(np.all(v_lif_wta[wta_neg_spk_idxs] == -1)) + self.assertTrue(np.all(v_lif_sig[sig_neg_spk_idxs] == -1)) + + def test_scif_fp_no_noise_interrupt_rfct_mid(self) -> None: + """ + Test a single SCIF neuron without LFSR noise, but with a constant bias. + + An inhibitory spike is injected in the middle of the refractory + period after the neuron spikes for the first time. The inhibition + interrupts the refractory period. The neuron issues negative spikes + at WTA and Sigma axons on consecutive time-steps. + + An excitatory spike is injected to nullify the inhibition and neuron + starts spiking periodically again. + """ + num_neurons = np.random.randint(1, 11) + bias = 1 + theta = 4 + neg_tau_ref = -5 + wt = 2 + t_inj_spk = {7: 1, 11: -1} + inj_times = list(t_inj_spk.keys()) + total_period = (theta // bias) - neg_tau_ref + num_epochs = 5 + num_steps = (theta // bias) + num_epochs * total_period + inj_times[1] + v_scif, v_lif_wta, v_lif_sig = self.run_test(num_steps=num_steps, + num_neurons=num_neurons, + bias=bias, + theta=theta, + neg_tau_ref=neg_tau_ref, + wt=wt, + t_inj_spk=t_inj_spk) + # Test pre-inhibitory-injection SCIF voltage and spiking + spk_idxs_pre_inj = np.array([theta // bias]).astype(int) - 1 + wta_pos_spk_pre_inj = spk_idxs_pre_inj + 1 + sig_pos_spk_pre_inj = wta_pos_spk_pre_inj + 1 + inh_inj = inj_times[0] + wta_neg_spk_rfct_interrupt = inh_inj + 1 + sig_neg_spk_rfct_interrupt = wta_neg_spk_rfct_interrupt + 1 + self.assertTrue(np.all(v_scif[spk_idxs_pre_inj] == neg_tau_ref)) + self.assertTrue(np.all(v_lif_wta[wta_pos_spk_pre_inj] == 1)) + self.assertTrue(np.all(v_lif_sig[sig_pos_spk_pre_inj] == 1)) + self.assertTrue(np.all(v_scif[inh_inj] == 0)) + self.assertTrue(np.all(v_lif_wta[wta_neg_spk_rfct_interrupt] == -1)) + self.assertTrue(np.all(v_lif_sig[sig_neg_spk_rfct_interrupt] == -1)) + # Test post-inhibitory-injection SCIF voltage and spiking + spk_idxs_post_inj = np.array([inj_times[1] + (theta // bias) - 1 + + j * total_period for j in range( + num_epochs)]).astype(int) + wta_pos_spk_idxs = spk_idxs_post_inj + 1 + sig_pos_spk_idxs = wta_pos_spk_idxs + 1 + wta_neg_spk_idxs = wta_pos_spk_idxs - neg_tau_ref + sig_neg_spk_idxs = sig_pos_spk_idxs - neg_tau_ref + self.assertTrue(np.all(v_scif[spk_idxs_post_inj] == neg_tau_ref)) + self.assertTrue(np.all(v_lif_wta[wta_pos_spk_idxs] == 1)) + self.assertTrue(np.all(v_lif_sig[sig_pos_spk_idxs] == 1)) + self.assertTrue(np.all(v_lif_wta[wta_neg_spk_idxs] == -1)) + self.assertTrue(np.all(v_lif_sig[sig_neg_spk_idxs] == -1)) + + def test_scif_fp_no_noise_interrupt_rfct_beg(self) -> None: + """ + Test a single SCIF neuron without LFSR noise, but with a constant bias. + + An inhibitory spike is injected at the very start of the refractory + period after the neuron spikes for the first time. The inhibition + interrupts the refractory period. The neuron issues a negative spike + at WTA axons to nullify its positive spike. No spikes are issued on + the Sigma axon. + + An excitatory spike is injected to nullify the inhibition and neuron + starts spiking periodically again. + """ + num_neurons = np.random.randint(1, 11) + bias = 1 + theta = 4 + neg_tau_ref = -5 + wt = 2 + t_inj_spk = {4: 1, 8: -1} + inj_times = list(t_inj_spk.keys()) + total_period = theta // bias - neg_tau_ref + num_epochs = 5 + num_steps = num_epochs * total_period + (theta // bias) + inj_times[1] + v_scif, v_lif_wta, v_lif_sig = self.run_test(num_steps=num_steps, + num_neurons=num_neurons, + bias=bias, + theta=theta, + neg_tau_ref=neg_tau_ref, + wt=wt, + t_inj_spk=t_inj_spk) + # Test pre-inhibitory-injection SCIF voltage and spiking + spk_idxs_pre_inj = np.array([theta // bias]).astype(int) - 1 + wta_pos_spk_pre_inj = spk_idxs_pre_inj + 1 + wta_neg_spk_pre_inj = wta_pos_spk_pre_inj + 1 + inh_inj = inj_times[0] + self.assertTrue(np.all(v_scif[spk_idxs_pre_inj] == neg_tau_ref)) + self.assertTrue(np.all(v_lif_wta[wta_pos_spk_pre_inj] == 1)) + self.assertTrue(np.all(v_lif_wta[wta_neg_spk_pre_inj] == -1)) + self.assertTrue(np.all(v_lif_sig[wta_neg_spk_pre_inj] == 0)) + self.assertTrue(np.all(v_scif[inh_inj] == 0)) + + # Test post-inhibitory-injection SCIF voltage and spiking + spk_idxs_post_inj = np.array([inj_times[1] + (theta // bias) - 1 + + j * total_period for j in range( + num_epochs)]).astype(int) + wta_pos_spk_idxs = spk_idxs_post_inj + 1 + sig_pos_spk_idxs = wta_pos_spk_idxs + 1 + wta_neg_spk_idxs = wta_pos_spk_idxs - neg_tau_ref + sig_neg_spk_idxs = sig_pos_spk_idxs - neg_tau_ref + self.assertTrue(np.all(v_scif[spk_idxs_post_inj] == neg_tau_ref)) + self.assertTrue(np.all(v_lif_wta[wta_pos_spk_idxs] == 1)) + self.assertTrue(np.all(v_lif_sig[sig_pos_spk_idxs] == 1)) + self.assertTrue(np.all(v_lif_wta[wta_neg_spk_idxs] == -1)) + self.assertTrue(np.all(v_lif_sig[sig_neg_spk_idxs] == -1)) diff --git a/tests/lava/proc/scif/test_process.py b/tests/lava/proc/scif/test_process.py new file mode 100644 index 000000000..ea55e7c4a --- /dev/null +++ b/tests/lava/proc/scif/test_process.py @@ -0,0 +1,34 @@ + +# INTEL CORPORATION CONFIDENTIAL AND PROPRIETARY +# +# Copyright © 2021-2022 Intel Corporation. +# +# This software and the related documents are Intel copyrighted +# materials, and your use of them is governed by the express +# license under which they were provided to you (License). Unless +# the License provides otherwise, you may not use, modify, copy, +# publish, distribute, disclose or transmit this software or the +# related documents without Intel's prior written permission. +# +# This software and the related documents are provided as is, with +# no express or implied warranties, other than those that are +# expressly stated in the License. +import unittest +from lava.proc.scif.process import SCIF + + +class TestSCIFProcess(unittest.TestCase): + """Tests for SigmaDelta class""" + def test_init(self) -> None: + """Tests instantiation of SigmaDelta""" + scif = SCIF(shape=(10,), + bias=2, + theta=8, + neg_tau_ref=-10, + enable_noise=1) + + self.assertEqual(scif.shape, (10,)) + self.assertEqual(scif.bias.init, 2) + self.assertEqual(scif.theta.init, 8) + self.assertEqual(scif.neg_tau_ref.init, -10) + self.assertEqual(scif.enable_noise.init, 1) From 09ae94f8ff8eca2addfab58cc0bf20d823cc5e95 Mon Sep 17 00:00:00 2001 From: "Risbud, Sumedh" Date: Wed, 7 Sep 2022 13:37:34 -0700 Subject: [PATCH 2/5] SCIF neuron model for QUBO, CSP, etc. Signed-off-by: Risbud, Sumedh --- src/lava/proc/scif/models.py | 130 ++++++++++++++ src/lava/proc/scif/process.py | 58 ++++++ tests/lava/proc/scif/__init__.py | 0 tests/lava/proc/scif/test_models.py | 257 +++++++++++++++++++++++++++ tests/lava/proc/scif/test_process.py | 32 ++++ 5 files changed, 477 insertions(+) create mode 100644 src/lava/proc/scif/models.py create mode 100644 src/lava/proc/scif/process.py create mode 100644 tests/lava/proc/scif/__init__.py create mode 100644 tests/lava/proc/scif/test_models.py create mode 100644 tests/lava/proc/scif/test_process.py diff --git a/src/lava/proc/scif/models.py b/src/lava/proc/scif/models.py new file mode 100644 index 000000000..7b26b3ac8 --- /dev/null +++ b/src/lava/proc/scif/models.py @@ -0,0 +1,130 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# See: https://spdx.org/licenses/ + +import numpy as np + +from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol +from lava.magma.core.model.py.ports import PyInPort, PyOutPort +from lava.magma.core.model.py.type import LavaPyType +from lava.magma.core.resources import CPU +from lava.magma.core.decorator import implements, requires, tag +from lava.magma.core.model.py.model import PyLoihiProcessModel +from lava.magma.core.model.lfsr_model import adv_lfsr_nbits +from lava.proc.scif.process import SCIF + + +@implements(proc=SCIF, protocol=LoihiProtocol) +@requires(CPU) +@tag('fixed_pt') +class PySCIFModelFixed(PyLoihiProcessModel): + """Fixed point implementation of Stochastic Constraint Integrate and + Fire (SCIF) neuron. + """ + a_in = LavaPyType(PyInPort.VEC_DENSE, int, precision=8) + s_sig_out = LavaPyType(PyOutPort.VEC_DENSE, int, precision=8) + s_wta_out = LavaPyType(PyOutPort.VEC_DENSE, int, precision=8) + + u: np.ndarray = LavaPyType(np.ndarray, int, precision=24) + v: np.ndarray = LavaPyType(np.ndarray, int, precision=24) + beta: np.ndarray = LavaPyType(np.ndarray, int, precision=8) + + bias: np.ndarray = LavaPyType(np.ndarray, int, precision=24) + theta: np.ndarray = LavaPyType(np.ndarray, int, precision=24) + neg_tau_ref: np.ndarray = LavaPyType(np.ndarray, int, precision=24) + enable_noise: np.ndarray = LavaPyType(np.ndarray, int, precision=1) + + def run_spk(self) -> None: + # Receive synaptic input + a_in_data = self.a_in.recv() + + # Define spike vectors + s_sig = np.zeros_like(self.v) + s_wta = np.zeros_like(self.v) + + # Saturated add the incoming activation + self.u += a_in_data + self.u[self.u > 2 ** 23 - 1] = 2 ** 23 - 1 + self.u[self.u < -2 ** 23] = -2 ** 23 + + # Populate the buffer for local computation + lsb = self.beta.copy() + lsb &= 3 + self.beta <<= 2 + self.beta[self.beta >= 256] = 0 # Overflow 8-bit unsigned beta to 0 + + # Gather spike and unsatisfied indices for summation axons + sig_unsat_idx = np.where(lsb == 2) + sig_spk_idx = np.where(np.logical_and(lsb == 1, self.u == 0)) + + # First set of unsatisfied WTA indices based on beta and u + wta_unsat_idx = np.where(np.logical_and(lsb == 1, self.u < 0)) + + # Reset voltages of unsatisfied WTA + self.v[wta_unsat_idx] = 0 + + # Assign sigma spikes (+/- 1) + s_sig[sig_unsat_idx] = -1 + s_sig[sig_spk_idx] = 1 + + # Determine neurons under refractory and not refractory + rfct_idx = np.where(self.v < 0) # indices of neurons in refractory + not_rfct_idx = np.where(self.v >= 0) # neurons not in refractory + + # Split/fork state variables u, v, beta + v_in_rfct = self.v[rfct_idx] # voltages in refractory + u_in_rfct = self.u[rfct_idx] # currents in refractory + beta_in_rfct = self.beta[rfct_idx] # beta in refractory + v_to_intg = self.v[not_rfct_idx] # voltages to be integrated + u_to_intg = self.u[not_rfct_idx] # currents to be integrated + beta_to_intg = self.beta[not_rfct_idx] # beta to be integrated + bias_to_intg = self.bias[not_rfct_idx] # bias to be integrated + + # Integration of constraints + # ToDo: Choosing a 16-bit signed random integer. For bit-accuracy, + # need to replace it with Loihi-conformant LFSR function + # If noise is enabled, choose a 16-bit signed random integer, + # else choose zeros + lfsr = np.zeros_like(v_to_intg) + if lfsr.size > 0: + rand_nums = \ + np.random.randint(-2 ** 15, 2 ** 15 - 1, + size=np.count_nonzero(self.enable_noise == 1)) + lfsr[self.enable_noise == 1] = rand_nums + + lfsr = np.right_shift(lfsr, 1) + v_to_intg = v_to_intg + lfsr + u_to_intg + bias_to_intg + v_to_intg[v_to_intg > 2 ** 23 - 1] = 2 ** 23 - 1 # Saturate at max + v_to_intg[v_to_intg < 0] = 0 # Remove negatives + + # WTA spike indices when threshold is exceeded + wta_spk_idx = np.where(v_to_intg >= self.theta) # Exceeds threshold + + # Spiking neuron voltages go in refractory + v_to_intg[wta_spk_idx] = self.neg_tau_ref # Post spk refractory + beta_to_intg[wta_spk_idx] |= 1 + s_wta[wta_spk_idx] = 1 # issue +1 WTA spikes + + # Refractory dynamics + v_in_rfct += 1 # voltage increments by 1 every step + beta_in_rfct |= 3 + # Second set of unsatisfied WTA indices based on v and u in refractory + wta_unsat_idx_2 = np.where(np.logical_or(v_in_rfct == 0, u_in_rfct < 0)) + + # Reset voltage of unsatisfied WTA in refractory + v_in_rfct[wta_unsat_idx_2] = 0 + beta_in_rfct[wta_unsat_idx_2] &= 2 + s_wta[wta_unsat_idx] = -1 + s_wta[wta_unsat_idx_2] = -1 + + # Assign all temporary states to state Vars + self.v[rfct_idx] = v_in_rfct + self.v[not_rfct_idx] = v_to_intg + self.u[rfct_idx] = u_in_rfct + self.u[not_rfct_idx] = u_to_intg + self.beta[rfct_idx] = beta_in_rfct + self.beta[not_rfct_idx] = beta_to_intg + + # Send out spikes + self.s_sig_out.send(s_sig) + self.s_wta_out.send(s_wta) diff --git a/src/lava/proc/scif/process.py b/src/lava/proc/scif/process.py new file mode 100644 index 000000000..94b55243c --- /dev/null +++ b/src/lava/proc/scif/process.py @@ -0,0 +1,58 @@ +# Copyright (C) 2021-22 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# See: https://spdx.org/licenses/ + +import typing as ty + +import numpy as np + +from lava.magma.core.process.process import AbstractProcess +from lava.magma.core.process.variable import Var +from lava.magma.core.process.ports.ports import InPort, OutPort + + +class SCIF(AbstractProcess): + + def __init__( + self, + *, + shape: ty.Tuple[int, ...], + bias: ty.Optional[int] = 1, + theta: ty.Optional[int] = 4, + neg_tau_ref: ty.Optional[int] = -5) -> None: + """ + Stochastic Constraint Integrate and Fire neuron Process. + + Parameters + ---------- + shape: Tuple + shape of the sigma process. Default is (1,). + bias: int + bias current driving the SCIF neuron. Default is 1 (arbitrary). + theta: int + threshold above which a SCIF neuron would fire winner-take-all + spike. Default is 4 (arbitrary). + neg_tau_ref: int + refractory time period (in number of algorithmic time-steps) for a + SCIF neuron after firing winner-take-all spike. Default is -5 ( + arbitrary). + """ + super().__init__(shape=shape) + + self.a_in = InPort(shape=shape) + self.s_sig_out = OutPort(shape=shape) + self.s_wta_out = OutPort(shape=shape) + + self.u = Var(shape=shape, init=np.zeros(shape=shape).astype(int)) + self.v = Var(shape=shape, init=np.zeros(shape=shape).astype(int)) + self.beta = Var(shape=shape, init=np.zeros(shape=shape).astype(int)) + self.enable_noise = Var(shape=shape, init=np.zeros( + shape=shape).astype(int)) + + self.bias = Var(shape=shape, init=int(bias)) + self.theta = Var(shape=(1,), init=int(theta)) + self.neg_tau_ref = Var(shape=(1,), init=int(neg_tau_ref)) + + @property + def shape(self) -> ty.Tuple[int, ...]: + return self.proc_params['shape'] diff --git a/tests/lava/proc/scif/__init__.py b/tests/lava/proc/scif/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/lava/proc/scif/test_models.py b/tests/lava/proc/scif/test_models.py new file mode 100644 index 000000000..2876ba1f7 --- /dev/null +++ b/tests/lava/proc/scif/test_models.py @@ -0,0 +1,257 @@ + +# INTEL CORPORATION CONFIDENTIAL AND PROPRIETARY +# +# Copyright © 2021-2022 Intel Corporation. +# +# This software and the related documents are Intel copyrighted +# materials, and your use of them is governed by the express +# license under which they were provided to you (License). Unless +# the License provides otherwise, you may not use, modify, copy, +# publish, distribute, disclose or transmit this software or the +# related documents without Intel's prior written permission. +# +# This software and the related documents are provided as is, with +# no express or implied warranties, other than those that are +# expressly stated in the License. +import sys +import unittest +import numpy as np +from typing import Tuple, Dict + +from lava.magma.core.run_configs import Loihi2SimCfg +from lava.magma.core.run_conditions import RunSteps +from lava.proc.scif.process import SCIF +from lava.proc.lif.process import LIF +from lava.proc.dense.process import Dense +from lava.proc.io.source import RingBuffer as SpikeSource + +verbose = True if (('-v' in sys.argv) or ('--verbose' in sys.argv)) else False + + +class TestSCIFModels(unittest.TestCase): + """Tests for sigma delta neuron""" + + def run_test( + self, + num_steps: int, + num_neurons: int, + bias: int, + theta: int, + neg_tau_ref: int, + wt: int, + t_inj_spk: Dict[int, int], # time_step -> payload dict to inject + tag: str = 'fixed_pt' + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + + spk_src = SpikeSource(data=np.array([[0] * num_neurons]).reshape( + num_neurons, 1).astype(int)) + dense_in = Dense(weights=(-1) * np.eye(num_neurons), + num_message_bits=16) + scif = SCIF(shape=(num_neurons,), + bias=bias, + theta=theta, + neg_tau_ref=neg_tau_ref) + dense_wta = Dense(weights=wt * np.eye(num_neurons), + num_message_bits=16) + dense_sig = Dense(weights=wt * np.eye(num_neurons), + num_message_bits=16) + lif_wta = LIF(shape=(num_neurons,), + du=4095, + dv=4096, + bias_mant=0, + vth=2**17-1) + lif_sig = LIF(shape=(num_neurons,), + du=4095, + dv=4096, + bias_mant=0, + vth=2**17-1) + spk_src.s_out.connect(dense_in.s_in) + dense_in.a_out.connect(scif.a_in) + scif.s_wta_out.connect(dense_wta.s_in) + scif.s_sig_out.connect(dense_sig.s_in) + dense_wta.a_out.connect(lif_wta.a_in) + dense_sig.a_out.connect(lif_sig.a_in) + + run_condition = RunSteps(num_steps=1) + run_config = Loihi2SimCfg(select_tag=tag) + + volts_scif = [] + volts_lif_wta = [] + volts_lif_sig = [] + for j in range(num_steps): + if j + 1 in t_inj_spk: + spk_src.data.set(np.array([[t_inj_spk[j + 1]] * + num_neurons]).astype(int)) + scif.run(condition=run_condition, run_cfg=run_config) + spk_src.data.set(np.array([[0] * num_neurons]).astype(int)) + volts_scif.append(scif.v.get()) + # Get the voltage of LIF attached to WTA + v_wta = lif_wta.v.get() + # Transform the voltage into +/- 1 spike + v_wta = (v_wta / wt).astype(int) # De-scale the weight + v_wta = np.right_shift(v_wta, 6) # downshift DendAccum's effect + # Append to list + volts_lif_wta.append(v_wta) + # Get the voltage of LIF attached to Sig + v_sig = lif_sig.v.get() + # Transform the voltage into +/- 1 spike + v_sig = (v_sig / wt).astype(int) # De-scale the weight + v_sig = np.right_shift(v_sig, 6) # downshift DendAccum's effect + # Append to list + volts_lif_sig.append(v_sig) + + scif.stop() + + return np.array(volts_scif).astype(int), \ + np.array(volts_lif_wta).astype(int), \ + np.array(volts_lif_sig).astype(int) + + def test_scif_fixed_pt_no_noise(self) -> None: + """Test a single SCIF neuron without noise, but with a constant bias. + The neuron is expected to spike with a regular period, on WTA as well as + Sigma axons. After excitatory spikes on two consecutive time-steps, the + neuron goes into inhibition and sends 2 inhibitory spikes of payload -1 + at the end of its refractory period. + """ + num_neurons = np.random.randint(1, 11) + bias = 1 + theta = 4 + neg_tau_ref = -5 + wt = 2 + total_period = theta // bias - neg_tau_ref + num_epochs = 10 + num_steps = num_epochs * total_period + (theta // bias) + v_scif, v_lif_wta, v_lif_sig = self.run_test(num_steps=num_steps, + num_neurons=num_neurons, + bias=bias, + theta=theta, + neg_tau_ref=neg_tau_ref, + wt=wt, + t_inj_spk={}) + # voltages = np.hstack((np.arange(num_steps).reshape(num_steps, 1), + # v_scif, + # v_lif_wta, + # v_lif_sig)) + # print(voltages) + spk_idxs = np.array([theta // bias - 1 + j * total_period for j in + range(num_epochs)]).astype(int) + wta_pos_spk_idxs = spk_idxs + 1 + sig_pos_spk_idxs = wta_pos_spk_idxs + 1 + wta_neg_spk_idxs = wta_pos_spk_idxs - neg_tau_ref + sig_neg_spk_idxs = sig_pos_spk_idxs - neg_tau_ref + self.assertTrue(np.all(v_scif[spk_idxs] == neg_tau_ref)) + self.assertTrue(np.all(v_lif_wta[wta_pos_spk_idxs] == 1)) + self.assertTrue(np.all(v_lif_sig[sig_pos_spk_idxs] == 1)) + self.assertTrue(np.all(v_lif_wta[wta_neg_spk_idxs] == -1)) + self.assertTrue(np.all(v_lif_sig[sig_neg_spk_idxs] == -1)) + + def test_scif_fp_no_noise_interrupt_rfct_mid(self) -> None: + """ + Test a single SCIF neuron without LFSR noise, but with a constant bias. + + An inhibitory spike is injected in the middle of the refractory + period after the neuron spikes for the first time. The inhibition + interrupts the refractory period. The neuron issues negative spikes + at WTA and Sigma axons on consecutive time-steps. + + An excitatory spike is injected to nullify the inhibition and neuron + starts spiking periodically again. + """ + num_neurons = np.random.randint(1, 11) + bias = 1 + theta = 4 + neg_tau_ref = -5 + wt = 2 + t_inj_spk = {7: 1, 11: -1} + inj_times = list(t_inj_spk.keys()) + total_period = (theta // bias) - neg_tau_ref + num_epochs = 5 + num_steps = (theta // bias) + num_epochs * total_period + inj_times[1] + v_scif, v_lif_wta, v_lif_sig = self.run_test(num_steps=num_steps, + num_neurons=num_neurons, + bias=bias, + theta=theta, + neg_tau_ref=neg_tau_ref, + wt=wt, + t_inj_spk=t_inj_spk) + # Test pre-inhibitory-injection SCIF voltage and spiking + spk_idxs_pre_inj = np.array([theta // bias]).astype(int) - 1 + wta_pos_spk_pre_inj = spk_idxs_pre_inj + 1 + sig_pos_spk_pre_inj = wta_pos_spk_pre_inj + 1 + inh_inj = inj_times[0] + wta_neg_spk_rfct_interrupt = inh_inj + 1 + sig_neg_spk_rfct_interrupt = wta_neg_spk_rfct_interrupt + 1 + self.assertTrue(np.all(v_scif[spk_idxs_pre_inj] == neg_tau_ref)) + self.assertTrue(np.all(v_lif_wta[wta_pos_spk_pre_inj] == 1)) + self.assertTrue(np.all(v_lif_sig[sig_pos_spk_pre_inj] == 1)) + self.assertTrue(np.all(v_scif[inh_inj] == 0)) + self.assertTrue(np.all(v_lif_wta[wta_neg_spk_rfct_interrupt] == -1)) + self.assertTrue(np.all(v_lif_sig[sig_neg_spk_rfct_interrupt] == -1)) + # Test post-inhibitory-injection SCIF voltage and spiking + spk_idxs_post_inj = np.array([inj_times[1] + (theta // bias) - 1 + + j * total_period for j in range( + num_epochs)]).astype(int) + wta_pos_spk_idxs = spk_idxs_post_inj + 1 + sig_pos_spk_idxs = wta_pos_spk_idxs + 1 + wta_neg_spk_idxs = wta_pos_spk_idxs - neg_tau_ref + sig_neg_spk_idxs = sig_pos_spk_idxs - neg_tau_ref + self.assertTrue(np.all(v_scif[spk_idxs_post_inj] == neg_tau_ref)) + self.assertTrue(np.all(v_lif_wta[wta_pos_spk_idxs] == 1)) + self.assertTrue(np.all(v_lif_sig[sig_pos_spk_idxs] == 1)) + self.assertTrue(np.all(v_lif_wta[wta_neg_spk_idxs] == -1)) + self.assertTrue(np.all(v_lif_sig[sig_neg_spk_idxs] == -1)) + + def test_scif_fp_no_noise_interrupt_rfct_beg(self) -> None: + """ + Test a single SCIF neuron without LFSR noise, but with a constant bias. + + An inhibitory spike is injected at the very start of the refractory + period after the neuron spikes for the first time. The inhibition + interrupts the refractory period. The neuron issues a negative spike + at WTA axons to nullify its positive spike. No spikes are issued on + the Sigma axon. + + An excitatory spike is injected to nullify the inhibition and neuron + starts spiking periodically again. + """ + num_neurons = np.random.randint(1, 11) + bias = 1 + theta = 4 + neg_tau_ref = -5 + wt = 2 + t_inj_spk = {4: 1, 8: -1} + inj_times = list(t_inj_spk.keys()) + total_period = theta // bias - neg_tau_ref + num_epochs = 5 + num_steps = num_epochs * total_period + (theta // bias) + inj_times[1] + v_scif, v_lif_wta, v_lif_sig = self.run_test(num_steps=num_steps, + num_neurons=num_neurons, + bias=bias, + theta=theta, + neg_tau_ref=neg_tau_ref, + wt=wt, + t_inj_spk=t_inj_spk) + # Test pre-inhibitory-injection SCIF voltage and spiking + spk_idxs_pre_inj = np.array([theta // bias]).astype(int) - 1 + wta_pos_spk_pre_inj = spk_idxs_pre_inj + 1 + wta_neg_spk_pre_inj = wta_pos_spk_pre_inj + 1 + inh_inj = inj_times[0] + self.assertTrue(np.all(v_scif[spk_idxs_pre_inj] == neg_tau_ref)) + self.assertTrue(np.all(v_lif_wta[wta_pos_spk_pre_inj] == 1)) + self.assertTrue(np.all(v_lif_wta[wta_neg_spk_pre_inj] == -1)) + self.assertTrue(np.all(v_lif_sig[wta_neg_spk_pre_inj] == 0)) + self.assertTrue(np.all(v_scif[inh_inj] == 0)) + + # Test post-inhibitory-injection SCIF voltage and spiking + spk_idxs_post_inj = np.array([inj_times[1] + (theta // bias) - 1 + + j * total_period for j in range( + num_epochs)]).astype(int) + wta_pos_spk_idxs = spk_idxs_post_inj + 1 + sig_pos_spk_idxs = wta_pos_spk_idxs + 1 + wta_neg_spk_idxs = wta_pos_spk_idxs - neg_tau_ref + sig_neg_spk_idxs = sig_pos_spk_idxs - neg_tau_ref + self.assertTrue(np.all(v_scif[spk_idxs_post_inj] == neg_tau_ref)) + self.assertTrue(np.all(v_lif_wta[wta_pos_spk_idxs] == 1)) + self.assertTrue(np.all(v_lif_sig[sig_pos_spk_idxs] == 1)) + self.assertTrue(np.all(v_lif_wta[wta_neg_spk_idxs] == -1)) + self.assertTrue(np.all(v_lif_sig[sig_neg_spk_idxs] == -1)) diff --git a/tests/lava/proc/scif/test_process.py b/tests/lava/proc/scif/test_process.py new file mode 100644 index 000000000..6353f7ac9 --- /dev/null +++ b/tests/lava/proc/scif/test_process.py @@ -0,0 +1,32 @@ + +# INTEL CORPORATION CONFIDENTIAL AND PROPRIETARY +# +# Copyright © 2021-2022 Intel Corporation. +# +# This software and the related documents are Intel copyrighted +# materials, and your use of them is governed by the express +# license under which they were provided to you (License). Unless +# the License provides otherwise, you may not use, modify, copy, +# publish, distribute, disclose or transmit this software or the +# related documents without Intel's prior written permission. +# +# This software and the related documents are provided as is, with +# no express or implied warranties, other than those that are +# expressly stated in the License. +import unittest +from lava.proc.scif.process import SCIF + + +class TestSCIFProcess(unittest.TestCase): + """Tests for SigmaDelta class""" + def test_init(self) -> None: + """Tests instantiation of SigmaDelta""" + scif = SCIF(shape=(10,), + bias=2, + theta=8, + neg_tau_ref=-10) + + self.assertEqual(scif.shape, (10,)) + self.assertEqual(scif.bias.init, 2) + self.assertEqual(scif.theta.init, 8) + self.assertEqual(scif.neg_tau_ref.init, -10) From ebe7b81d75220326afa0c2c47ca3c47e7afea606 Mon Sep 17 00:00:00 2001 From: "Risbud, Sumedh" Date: Wed, 7 Sep 2022 15:21:59 -0700 Subject: [PATCH 3/5] Minor import fix Signed-off-by: Risbud, Sumedh --- src/lava/proc/scif/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lava/proc/scif/models.py b/src/lava/proc/scif/models.py index 7b26b3ac8..019dbee0f 100644 --- a/src/lava/proc/scif/models.py +++ b/src/lava/proc/scif/models.py @@ -10,7 +10,6 @@ from lava.magma.core.resources import CPU from lava.magma.core.decorator import implements, requires, tag from lava.magma.core.model.py.model import PyLoihiProcessModel -from lava.magma.core.model.lfsr_model import adv_lfsr_nbits from lava.proc.scif.process import SCIF From e24805e76a86f5e89fdaeab63c484556b2c2c98e Mon Sep 17 00:00:00 2001 From: "Risbud, Sumedh" Date: Wed, 21 Sep 2022 03:58:44 -0700 Subject: [PATCH 4/5] SCIF: Enabled QUBO support Signed-off-by: Risbud, Sumedh --- src/lava/proc/scif/models.py | 248 ++++++++++++++++++--------- src/lava/proc/scif/process.py | 60 ++++++- tests/lava/proc/scif/test_models.py | 251 ++++++++++++++++++++++++---- 3 files changed, 436 insertions(+), 123 deletions(-) diff --git a/src/lava/proc/scif/models.py b/src/lava/proc/scif/models.py index 019dbee0f..472912144 100644 --- a/src/lava/proc/scif/models.py +++ b/src/lava/proc/scif/models.py @@ -1,6 +1,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # See: https://spdx.org/licenses/ +from abc import abstractmethod import numpy as np @@ -10,120 +11,203 @@ from lava.magma.core.resources import CPU from lava.magma.core.decorator import implements, requires, tag from lava.magma.core.model.py.model import PyLoihiProcessModel -from lava.proc.scif.process import SCIF +from lava.proc.scif.process import CspScif, QuboScif -@implements(proc=SCIF, protocol=LoihiProtocol) -@requires(CPU) -@tag('fixed_pt') -class PySCIFModelFixed(PyLoihiProcessModel): - """Fixed point implementation of Stochastic Constraint Integrate and - Fire (SCIF) neuron. +class AbstractPyModelScifFixed(PyLoihiProcessModel): + """Abstract implementation of fixed point precision + stochastic constraint integrate-and-fire neuron model. Implementations + such as those bit-accurate with Loihi hardware inherit from here. """ + a_in = LavaPyType(PyInPort.VEC_DENSE, int, precision=8) s_sig_out = LavaPyType(PyOutPort.VEC_DENSE, int, precision=8) s_wta_out = LavaPyType(PyOutPort.VEC_DENSE, int, precision=8) - u: np.ndarray = LavaPyType(np.ndarray, int, precision=24) - v: np.ndarray = LavaPyType(np.ndarray, int, precision=24) - beta: np.ndarray = LavaPyType(np.ndarray, int, precision=8) + cnstr_intg: np.ndarray = LavaPyType(np.ndarray, int, precision=24) + state: np.ndarray = LavaPyType(np.ndarray, int, precision=24) + spk_hist: np.ndarray = LavaPyType(np.ndarray, int, precision=8) - bias: np.ndarray = LavaPyType(np.ndarray, int, precision=24) + step_size: np.ndarray = LavaPyType(np.ndarray, int, precision=24) theta: np.ndarray = LavaPyType(np.ndarray, int, precision=24) neg_tau_ref: np.ndarray = LavaPyType(np.ndarray, int, precision=24) - enable_noise: np.ndarray = LavaPyType(np.ndarray, int, precision=1) + noise_ampl: np.ndarray = LavaPyType(np.ndarray, int, precision=1) - def run_spk(self) -> None: - # Receive synaptic input - a_in_data = self.a_in.recv() + def __init__(self, proc_params): + super(AbstractPyModelScifFixed, self).__init__(proc_params) + self.a_in_data = np.zeros(proc_params['shape']) - # Define spike vectors - s_sig = np.zeros_like(self.v) - s_wta = np.zeros_like(self.v) + def _prng(self, shape_like): + """Pseudo-random number generator + """ - # Saturated add the incoming activation - self.u += a_in_data - self.u[self.u > 2 ** 23 - 1] = 2 ** 23 - 1 - self.u[self.u < -2 ** 23] = -2 ** 23 + # ToDo: Choosing a 16-bit signed random integer. For bit-accuracy, + # need to replace it with Loihi-conformant LFSR function + prand = np.zeros_like(shape_like) + if prand.size > 0: + rand_nums = \ + np.random.randint(-2 ** 15, 2 ** 15 - 1, size=prand.size) + # Assign random numbers only to neurons, for which noise is enabled + prand = rand_nums * self.noise_ampl - # Populate the buffer for local computation - lsb = self.beta.copy() - lsb &= 3 - self.beta <<= 2 - self.beta[self.beta >= 256] = 0 # Overflow 8-bit unsigned beta to 0 + return prand - # Gather spike and unsatisfied indices for summation axons - sig_unsat_idx = np.where(lsb == 2) - sig_spk_idx = np.where(np.logical_and(lsb == 1, self.u == 0)) + def _update_buffers(self): + # !! Side effect: Changes self.beta !! - # First set of unsatisfied WTA indices based on beta and u - wta_unsat_idx = np.where(np.logical_and(lsb == 1, self.u < 0)) + # Populate the buffer for local computation + spk_hist_buffer = self.spk_hist.copy() + spk_hist_buffer &= 3 + self.spk_hist <<= 2 + # Overflow 8-bit unsigned beta to 0 + self.spk_hist[self.spk_hist >= 256] = 0 - # Reset voltages of unsatisfied WTA - self.v[wta_unsat_idx] = 0 + return spk_hist_buffer - # Assign sigma spikes (+/- 1) - s_sig[sig_unsat_idx] = -1 - s_sig[sig_spk_idx] = 1 + def _integration_dynamics(self, intg_idx): - # Determine neurons under refractory and not refractory - rfct_idx = np.where(self.v < 0) # indices of neurons in refractory - not_rfct_idx = np.where(self.v >= 0) # neurons not in refractory + state_to_intg = self.state[intg_idx] # voltages to be integrated + cnstr_to_intg = self.cnstr_intg[intg_idx] # currents to be integrated + spk_hist_to_intg = self.spk_hist[intg_idx] # beta to be integrated + step_size_to_intg = self.step_size[intg_idx] # bias to be integrated - # Split/fork state variables u, v, beta - v_in_rfct = self.v[rfct_idx] # voltages in refractory - u_in_rfct = self.u[rfct_idx] # currents in refractory - beta_in_rfct = self.beta[rfct_idx] # beta in refractory - v_to_intg = self.v[not_rfct_idx] # voltages to be integrated - u_to_intg = self.u[not_rfct_idx] # currents to be integrated - beta_to_intg = self.beta[not_rfct_idx] # beta to be integrated - bias_to_intg = self.bias[not_rfct_idx] # bias to be integrated - - # Integration of constraints - # ToDo: Choosing a 16-bit signed random integer. For bit-accuracy, - # need to replace it with Loihi-conformant LFSR function - # If noise is enabled, choose a 16-bit signed random integer, - # else choose zeros - lfsr = np.zeros_like(v_to_intg) - if lfsr.size > 0: - rand_nums = \ - np.random.randint(-2 ** 15, 2 ** 15 - 1, - size=np.count_nonzero(self.enable_noise == 1)) - lfsr[self.enable_noise == 1] = rand_nums + lfsr = self._prng(shape_like=state_to_intg) - lfsr = np.right_shift(lfsr, 1) - v_to_intg = v_to_intg + lfsr + u_to_intg + bias_to_intg - v_to_intg[v_to_intg > 2 ** 23 - 1] = 2 ** 23 - 1 # Saturate at max - v_to_intg[v_to_intg < 0] = 0 # Remove negatives + state_to_intg = state_to_intg + lfsr + cnstr_to_intg + step_size_to_intg + np.clip(state_to_intg, a_min=0, a_max=2 ** 23 - 1, out=state_to_intg) # WTA spike indices when threshold is exceeded - wta_spk_idx = np.where(v_to_intg >= self.theta) # Exceeds threshold + wta_spk_idx = np.where(state_to_intg >= self.theta) # Exceeds threshold + # Spiking neuron voltages go in refractory (if neg_tau_ref < 0) + state_to_intg[wta_spk_idx] = self.neg_tau_ref # Post spk refractory + spk_hist_to_intg[wta_spk_idx] |= 1 + + # Assign all temporary states to state Vars + self.state[intg_idx] = state_to_intg + self.cnstr_intg[intg_idx] = cnstr_to_intg + self.spk_hist[intg_idx] = spk_hist_to_intg - # Spiking neuron voltages go in refractory - v_to_intg[wta_spk_idx] = self.neg_tau_ref # Post spk refractory - beta_to_intg[wta_spk_idx] |= 1 - s_wta[wta_spk_idx] = 1 # issue +1 WTA spikes + return wta_spk_idx + + def _refractory_dynamics(self, rfct_idx): + + # Split/fork state variables u, v, beta + state_in_rfct = self.state[rfct_idx] # voltages in refractory + cnstr_in_rfct = self.cnstr_intg[rfct_idx] # currents in refractory + spk_hist_in_rfct = self.spk_hist[rfct_idx] # beta in refractory # Refractory dynamics - v_in_rfct += 1 # voltage increments by 1 every step - beta_in_rfct |= 3 + state_in_rfct += 1 # voltage increments by 1 every step + spk_hist_in_rfct |= 3 # Second set of unsatisfied WTA indices based on v and u in refractory - wta_unsat_idx_2 = np.where(np.logical_or(v_in_rfct == 0, u_in_rfct < 0)) + wta_unsat_idx_2 = \ + np.where(np.logical_or(state_in_rfct == 0, cnstr_in_rfct < 0)) # Reset voltage of unsatisfied WTA in refractory - v_in_rfct[wta_unsat_idx_2] = 0 - beta_in_rfct[wta_unsat_idx_2] &= 2 - s_wta[wta_unsat_idx] = -1 - s_wta[wta_unsat_idx_2] = -1 + state_in_rfct[wta_unsat_idx_2] = 0 + spk_hist_in_rfct[wta_unsat_idx_2] &= 2 # Assign all temporary states to state Vars - self.v[rfct_idx] = v_in_rfct - self.v[not_rfct_idx] = v_to_intg - self.u[rfct_idx] = u_in_rfct - self.u[not_rfct_idx] = u_to_intg - self.beta[rfct_idx] = beta_in_rfct - self.beta[not_rfct_idx] = beta_to_intg + self.state[rfct_idx] = state_in_rfct + self.cnstr_intg[rfct_idx] = cnstr_in_rfct + self.spk_hist[rfct_idx] = spk_hist_in_rfct + + return wta_unsat_idx_2 + + @abstractmethod + def _gen_sig_spks(self, spk_hist_buffer): + raise NotImplementedError("Abstract method not implemented for " + "abstract class.") + + def _gen_wta_spks(self, spk_hist_buffer): + # Indices of WTA neurons signifying unsatisfied constraints, based on + # buffered history from previous timestep + wta_unsat_prev_ts_idx = np.where(np.logical_and(spk_hist_buffer == 1, + self.cnstr_intg < 0)) + + # Reset voltages of unsatisfied WTA + self.state[wta_unsat_prev_ts_idx] = 0 + # indices of neurons to be integrated: + intg_idx = np.where(self.state >= 0) + # indices of neurons in refractory: + rfct_idx = np.where(self.state < 0) + + # Indices of WTA neurons that will spike and enter refractory + wta_spk_idx = self._integration_dynamics(intg_idx) + + # Indices of WTA neurons coming out of refractory or those signifying + # unsatisfied constraints + wta_rfct_end_or_unsat_idx = self._refractory_dynamics(rfct_idx) if \ + self.neg_tau_ref != 0 else (np.array([], dtype=np.int32),) + + s_wta = np.zeros_like(self.state) + s_wta[wta_spk_idx] = 1 + s_wta[wta_unsat_prev_ts_idx] = -1 + s_wta[wta_rfct_end_or_unsat_idx] = -1 + + return s_wta + + def run_spk(self) -> None: + # Receive synaptic input + self.a_in_data = self.a_in.recv() + + # Add the incoming activation and saturate to min-max limits + np.clip(self.cnstr_intg + self.a_in_data, a_min=-2 ** 23, + a_max=2 ** 23 - 1, out=self.cnstr_intg) + + # !! Side effect: Changes self.beta !! + spk_hist_buffer = self._update_buffers() + + # Generate Sigma spikes + s_sig = self._gen_sig_spks(spk_hist_buffer) + + # Generate WTA spikes + s_wta = self._gen_wta_spks(spk_hist_buffer) # Send out spikes self.s_sig_out.send(s_sig) self.s_wta_out.send(s_wta) + + +@implements(proc=CspScif, protocol=LoihiProtocol) +@requires(CPU) +@tag('fixed_pt') +class PyModelCspScifFixed(AbstractPyModelScifFixed): + """Fixed point implementation of Stochastic Constraint Integrate and + Fire (SCIF) neuron for solving CSP problems. + """ + + def _gen_sig_spks(self, spk_hist_buffer): + s_sig = np.zeros_like(self.state) + # Gather spike and unsatisfied indices for summation axons + sig_unsat_idx = np.where(spk_hist_buffer == 2) + sig_spk_idx = np.where(np.logical_and(spk_hist_buffer == 1, + self.cnstr_intg == 0)) + + # Assign sigma spikes (+/- 1) + s_sig[sig_unsat_idx] = -1 + s_sig[sig_spk_idx] = 1 + + return s_sig + + +@implements(proc=QuboScif, protocol=LoihiProtocol) +@requires(CPU) +@tag('fixed_pt') +class PyModelQuboScifFixed(AbstractPyModelScifFixed): + """Fixed point implementation of Stochastic Constraint Integrate and + Fire (SCIF) neuron for solving QUBO problems. + """ + + cost_diagonal: np.ndarray = LavaPyType(np.ndarray, int, precision=24) + + def _gen_sig_spks(self, spk_hist_buffer): + s_sig = np.zeros_like(self.state) + # If we have fired in the previous time-step, we send out the local + # cost now, i.e., when spk_hist_buffer == 1 + sig_spk_idx = np.where(spk_hist_buffer == 1) + # Compute the local cost + s_sig[sig_spk_idx] = self.cost_diagonal[sig_spk_idx] + \ + self.a_in_data[sig_spk_idx] + + return s_sig diff --git a/src/lava/proc/scif/process.py b/src/lava/proc/scif/process.py index 94b55243c..d4a4ae29c 100644 --- a/src/lava/proc/scif/process.py +++ b/src/lava/proc/scif/process.py @@ -3,6 +3,7 @@ # See: https://spdx.org/licenses/ import typing as ty +from numpy import typing as npty import numpy as np @@ -11,13 +12,16 @@ from lava.magma.core.process.ports.ports import InPort, OutPort -class SCIF(AbstractProcess): +class AbstractScif(AbstractProcess): + """Abstract Process for Stochastic Constraint Integrate-and-Fire + (SCIF) neurons. + """ def __init__( self, *, shape: ty.Tuple[int, ...], - bias: ty.Optional[int] = 1, + step_size: ty.Optional[int] = 1, theta: ty.Optional[int] = 4, neg_tau_ref: ty.Optional[int] = -5) -> None: """ @@ -26,8 +30,8 @@ def __init__( Parameters ---------- shape: Tuple - shape of the sigma process. Default is (1,). - bias: int + Number of neurons. Default is (1,). + step_size: int bias current driving the SCIF neuron. Default is 1 (arbitrary). theta: int threshold above which a SCIF neuron would fire winner-take-all @@ -43,16 +47,54 @@ def __init__( self.s_sig_out = OutPort(shape=shape) self.s_wta_out = OutPort(shape=shape) - self.u = Var(shape=shape, init=np.zeros(shape=shape).astype(int)) - self.v = Var(shape=shape, init=np.zeros(shape=shape).astype(int)) - self.beta = Var(shape=shape, init=np.zeros(shape=shape).astype(int)) - self.enable_noise = Var(shape=shape, init=np.zeros( + self.cnstr_intg = Var(shape=shape, init=np.zeros(shape=shape).astype( + int)) + self.state = Var(shape=shape, init=np.zeros(shape=shape).astype(int)) + self.spk_hist = Var(shape=shape, init=np.zeros(shape=shape).astype(int)) + self.noise_ampl = Var(shape=shape, init=np.zeros( shape=shape).astype(int)) - self.bias = Var(shape=shape, init=int(bias)) + self.step_size = Var(shape=shape, init=int(step_size)) self.theta = Var(shape=(1,), init=int(theta)) self.neg_tau_ref = Var(shape=(1,), init=int(neg_tau_ref)) @property def shape(self) -> ty.Tuple[int, ...]: return self.proc_params['shape'] + + +class CspScif(AbstractScif): + """Stochastic Constraint Integrate-and-Fire neurons to solve CSPs. + """ + + def __init__(self, + *, + shape: ty.Tuple[int, ...], + step_size: ty.Optional[int] = 1, + theta: ty.Optional[int] = 4, + neg_tau_ref: ty.Optional[int] = -5): + + super(CspScif, self).__init__(shape=shape, + step_size=step_size, + theta=theta, + neg_tau_ref=neg_tau_ref) + + +class QuboScif(AbstractScif): + """Stochastic Constraint Integrate-and-Fire neurons to solve QUBO + problems. + """ + + def __init__(self, + *, + shape: ty.Tuple[int, ...], + cost_diag: npty.NDArray, + step_size: ty.Optional[int] = 1, + theta: ty.Optional[int] = 4, + neg_tau_ref: ty.Optional[int] = -5): + + super(QuboScif, self).__init__(shape=shape, + step_size=step_size, + theta=theta, + neg_tau_ref=neg_tau_ref) + self.cost_diagonal = Var(shape=shape, init=cost_diag) diff --git a/tests/lava/proc/scif/test_models.py b/tests/lava/proc/scif/test_models.py index 2876ba1f7..f69d1ee94 100644 --- a/tests/lava/proc/scif/test_models.py +++ b/tests/lava/proc/scif/test_models.py @@ -20,7 +20,7 @@ from lava.magma.core.run_configs import Loihi2SimCfg from lava.magma.core.run_conditions import RunSteps -from lava.proc.scif.process import SCIF +from lava.proc.scif.process import CspScif, QuboScif from lava.proc.lif.process import LIF from lava.proc.dense.process import Dense from lava.proc.io.source import RingBuffer as SpikeSource @@ -28,14 +28,14 @@ verbose = True if (('-v' in sys.argv) or ('--verbose' in sys.argv)) else False -class TestSCIFModels(unittest.TestCase): +class TestCspScifModels(unittest.TestCase): """Tests for sigma delta neuron""" def run_test( self, num_steps: int, num_neurons: int, - bias: int, + step_size: int, theta: int, neg_tau_ref: int, wt: int, @@ -47,10 +47,10 @@ def run_test( num_neurons, 1).astype(int)) dense_in = Dense(weights=(-1) * np.eye(num_neurons), num_message_bits=16) - scif = SCIF(shape=(num_neurons,), - bias=bias, - theta=theta, - neg_tau_ref=neg_tau_ref) + csp_scif = CspScif(shape=(num_neurons,), + step_size=step_size, + theta=theta, + neg_tau_ref=neg_tau_ref) dense_wta = Dense(weights=wt * np.eye(num_neurons), num_message_bits=16) dense_sig = Dense(weights=wt * np.eye(num_neurons), @@ -59,16 +59,16 @@ def run_test( du=4095, dv=4096, bias_mant=0, - vth=2**17-1) + vth=2 ** 17 - 1) lif_sig = LIF(shape=(num_neurons,), du=4095, dv=4096, bias_mant=0, - vth=2**17-1) + vth=2 ** 17 - 1) spk_src.s_out.connect(dense_in.s_in) - dense_in.a_out.connect(scif.a_in) - scif.s_wta_out.connect(dense_wta.s_in) - scif.s_sig_out.connect(dense_sig.s_in) + dense_in.a_out.connect(csp_scif.a_in) + csp_scif.s_wta_out.connect(dense_wta.s_in) + csp_scif.s_sig_out.connect(dense_sig.s_in) dense_wta.a_out.connect(lif_wta.a_in) dense_sig.a_out.connect(lif_sig.a_in) @@ -82,9 +82,9 @@ def run_test( if j + 1 in t_inj_spk: spk_src.data.set(np.array([[t_inj_spk[j + 1]] * num_neurons]).astype(int)) - scif.run(condition=run_condition, run_cfg=run_config) + csp_scif.run(condition=run_condition, run_cfg=run_config) spk_src.data.set(np.array([[0] * num_neurons]).astype(int)) - volts_scif.append(scif.v.get()) + volts_scif.append(csp_scif.state.get()) # Get the voltage of LIF attached to WTA v_wta = lif_wta.v.get() # Transform the voltage into +/- 1 spike @@ -100,7 +100,7 @@ def run_test( # Append to list volts_lif_sig.append(v_sig) - scif.stop() + csp_scif.stop() return np.array(volts_scif).astype(int), \ np.array(volts_lif_wta).astype(int), \ @@ -114,16 +114,16 @@ def test_scif_fixed_pt_no_noise(self) -> None: at the end of its refractory period. """ num_neurons = np.random.randint(1, 11) - bias = 1 + step_size = 1 theta = 4 neg_tau_ref = -5 wt = 2 - total_period = theta // bias - neg_tau_ref + total_period = theta // step_size - neg_tau_ref num_epochs = 10 - num_steps = num_epochs * total_period + (theta // bias) + num_steps = num_epochs * total_period + (theta // step_size) v_scif, v_lif_wta, v_lif_sig = self.run_test(num_steps=num_steps, num_neurons=num_neurons, - bias=bias, + step_size=step_size, theta=theta, neg_tau_ref=neg_tau_ref, wt=wt, @@ -132,8 +132,9 @@ def test_scif_fixed_pt_no_noise(self) -> None: # v_scif, # v_lif_wta, # v_lif_sig)) + # np.set_printoptions(linewidth=np.inf, threshold=np.inf) # print(voltages) - spk_idxs = np.array([theta // bias - 1 + j * total_period for j in + spk_idxs = np.array([theta // step_size - 1 + j * total_period for j in range(num_epochs)]).astype(int) wta_pos_spk_idxs = spk_idxs + 1 sig_pos_spk_idxs = wta_pos_spk_idxs + 1 @@ -158,24 +159,25 @@ def test_scif_fp_no_noise_interrupt_rfct_mid(self) -> None: starts spiking periodically again. """ num_neurons = np.random.randint(1, 11) - bias = 1 + step_size = 1 theta = 4 neg_tau_ref = -5 wt = 2 t_inj_spk = {7: 1, 11: -1} inj_times = list(t_inj_spk.keys()) - total_period = (theta // bias) - neg_tau_ref + total_period = (theta // step_size) - neg_tau_ref num_epochs = 5 - num_steps = (theta // bias) + num_epochs * total_period + inj_times[1] + num_steps = \ + (theta // step_size) + num_epochs * total_period + inj_times[1] v_scif, v_lif_wta, v_lif_sig = self.run_test(num_steps=num_steps, num_neurons=num_neurons, - bias=bias, + step_size=step_size, theta=theta, neg_tau_ref=neg_tau_ref, wt=wt, t_inj_spk=t_inj_spk) # Test pre-inhibitory-injection SCIF voltage and spiking - spk_idxs_pre_inj = np.array([theta // bias]).astype(int) - 1 + spk_idxs_pre_inj = np.array([theta // step_size]).astype(int) - 1 wta_pos_spk_pre_inj = spk_idxs_pre_inj + 1 sig_pos_spk_pre_inj = wta_pos_spk_pre_inj + 1 inh_inj = inj_times[0] @@ -188,7 +190,7 @@ def test_scif_fp_no_noise_interrupt_rfct_mid(self) -> None: self.assertTrue(np.all(v_lif_wta[wta_neg_spk_rfct_interrupt] == -1)) self.assertTrue(np.all(v_lif_sig[sig_neg_spk_rfct_interrupt] == -1)) # Test post-inhibitory-injection SCIF voltage and spiking - spk_idxs_post_inj = np.array([inj_times[1] + (theta // bias) - 1 + + spk_idxs_post_inj = np.array([inj_times[1] + (theta // step_size) - 1 + j * total_period for j in range( num_epochs)]).astype(int) wta_pos_spk_idxs = spk_idxs_post_inj + 1 @@ -215,24 +217,25 @@ def test_scif_fp_no_noise_interrupt_rfct_beg(self) -> None: starts spiking periodically again. """ num_neurons = np.random.randint(1, 11) - bias = 1 + step_size = 1 theta = 4 neg_tau_ref = -5 wt = 2 t_inj_spk = {4: 1, 8: -1} inj_times = list(t_inj_spk.keys()) - total_period = theta // bias - neg_tau_ref + total_period = theta // step_size - neg_tau_ref num_epochs = 5 - num_steps = num_epochs * total_period + (theta // bias) + inj_times[1] + num_steps = \ + num_epochs * total_period + (theta // step_size) + inj_times[1] v_scif, v_lif_wta, v_lif_sig = self.run_test(num_steps=num_steps, num_neurons=num_neurons, - bias=bias, + step_size=step_size, theta=theta, neg_tau_ref=neg_tau_ref, wt=wt, t_inj_spk=t_inj_spk) # Test pre-inhibitory-injection SCIF voltage and spiking - spk_idxs_pre_inj = np.array([theta // bias]).astype(int) - 1 + spk_idxs_pre_inj = np.array([theta // step_size]).astype(int) - 1 wta_pos_spk_pre_inj = spk_idxs_pre_inj + 1 wta_neg_spk_pre_inj = wta_pos_spk_pre_inj + 1 inh_inj = inj_times[0] @@ -243,7 +246,7 @@ def test_scif_fp_no_noise_interrupt_rfct_beg(self) -> None: self.assertTrue(np.all(v_scif[inh_inj] == 0)) # Test post-inhibitory-injection SCIF voltage and spiking - spk_idxs_post_inj = np.array([inj_times[1] + (theta // bias) - 1 + + spk_idxs_post_inj = np.array([inj_times[1] + (theta // step_size) - 1 + j * total_period for j in range( num_epochs)]).astype(int) wta_pos_spk_idxs = spk_idxs_post_inj + 1 @@ -255,3 +258,187 @@ def test_scif_fp_no_noise_interrupt_rfct_beg(self) -> None: self.assertTrue(np.all(v_lif_sig[sig_pos_spk_idxs] == 1)) self.assertTrue(np.all(v_lif_wta[wta_neg_spk_idxs] == -1)) self.assertTrue(np.all(v_lif_sig[sig_neg_spk_idxs] == -1)) + + +class TestQuboScifModels(unittest.TestCase): + """Tests for sigma delta neuron""" + + def run_test( + self, + num_steps: int, + num_neurons: int, + cost_diag: np.ndarray, + step_size: int, + theta: int, + neg_tau_ref: int, + wt: int, + t_inj_spk: Dict[int, int], # time_step -> payload dict to inject + tag: str = 'fixed_pt' + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + + spk_src = SpikeSource(data=np.array([[0] * num_neurons]).reshape( + num_neurons, 1).astype(int)) + dense_in = Dense(weights=(-1) * np.eye(num_neurons), + num_message_bits=16) + qubo_scif = QuboScif(shape=(num_neurons,), + cost_diag=cost_diag, + step_size=step_size, + theta=theta, + neg_tau_ref=neg_tau_ref) + dense_wta = Dense(weights=wt * np.eye(num_neurons), + num_message_bits=16) + dense_sig = Dense(weights=wt * np.eye(num_neurons), + num_message_bits=16) + lif_wta = LIF(shape=(num_neurons,), + du=4095, + dv=4096, + bias_mant=0, + vth=2 ** 17 - 1) + lif_sig = LIF(shape=(num_neurons,), + du=4095, + dv=4096, + bias_mant=0, + vth=2 ** 17 - 1) + spk_src.s_out.connect(dense_in.s_in) + dense_in.a_out.connect(qubo_scif.a_in) + qubo_scif.s_wta_out.connect(dense_wta.s_in) + qubo_scif.s_sig_out.connect(dense_sig.s_in) + dense_wta.a_out.connect(lif_wta.a_in) + dense_sig.a_out.connect(lif_sig.a_in) + + run_condition = RunSteps(num_steps=1) + run_config = Loihi2SimCfg(select_tag=tag) + + volts_scif = [] + volts_lif_wta = [] + volts_lif_sig = [] + for j in range(num_steps): + if j + 1 in t_inj_spk: + spk_src.data.set(np.array([[t_inj_spk[j + 1]] * + num_neurons]).astype(int)) + qubo_scif.run(condition=run_condition, run_cfg=run_config) + spk_src.data.set(np.array([[0] * num_neurons]).astype(int)) + volts_scif.append(qubo_scif.state.get()) + # Get the voltage of LIF attached to WTA + v_wta = lif_wta.v.get() + # Transform the voltage into +/- 1 spike + v_wta = (v_wta / wt).astype(int) # De-scale the weight + v_wta = np.right_shift(v_wta, 6) # downshift DendAccum's effect + # Append to list + volts_lif_wta.append(v_wta) + # Get the voltage of LIF attached to Sig + v_sig = lif_sig.v.get() + # Transform the voltage into +/- 1 spike + v_sig = (v_sig / wt).astype(int) # De-scale the weight + v_sig = np.right_shift(v_sig, 6) # downshift DendAccum's effect + # Append to list + volts_lif_sig.append(v_sig) + + qubo_scif.stop() + + return np.array(volts_scif).astype(int), \ + np.array(volts_lif_wta).astype(int), \ + np.array(volts_lif_sig).astype(int) + + def test_scif_fixed_pt_no_noise(self) -> None: + """Test a single SCIF neuron without noise, but with a constant bias. + The neuron is expected to spike with a regular period, on WTA as well as + Sigma axons. After excitatory spikes on two consecutive time-steps, the + neuron goes into inhibition and sends 2 inhibitory spikes of payload -1 + at the end of its refractory period. + """ + num_neurons = np.random.randint(1, 11) + cost_diag = np.arange(1, num_neurons + 1) + step_size = 1 + theta = 4 + neg_tau_ref = 0 + wt = 2 + total_period = theta // step_size - neg_tau_ref + num_epochs = 10 + num_steps = num_epochs * total_period + (theta // step_size) + v_scif, v_lif_wta, v_lif_sig = self.run_test(num_steps=num_steps, + num_neurons=num_neurons, + cost_diag=cost_diag, + step_size=step_size, + theta=theta, + neg_tau_ref=neg_tau_ref, + wt=wt, + t_inj_spk={}) + # voltages = np.hstack((np.arange(num_steps).reshape(num_steps, 1), + # v_scif, + # v_lif_wta, + # v_lif_sig)) + # np.set_printoptions(linewidth=np.inf, threshold=np.inf) + # print(voltages) + spk_idxs = np.array([theta // step_size - 1 + j * total_period for j in + range(num_epochs)]).astype(int) + wta_pos_spk_idxs = spk_idxs + 1 + sig_pos_spk_idxs = wta_pos_spk_idxs + 1 + self.assertTrue(np.all(v_scif[spk_idxs] == neg_tau_ref)) + self.assertTrue(np.all(v_lif_wta[wta_pos_spk_idxs] == 1)) + self.assertTrue(np.all(v_lif_sig[sig_pos_spk_idxs] == cost_diag)) + + def test_scif_fp_no_noise_interrupt_rfct_mid(self) -> None: + """ + Test a single SCIF neuron without LFSR noise, but with a constant bias. + + An inhibitory spike is injected in the middle of the refractory + period after the neuron spikes for the first time. The inhibition + interrupts the refractory period. The neuron issues negative spikes + at WTA and Sigma axons on consecutive time-steps. + + An excitatory spike is injected to nullify the inhibition and neuron + starts spiking periodically again. + """ + num_neurons = np.random.randint(1, 11) + cost_diag = np.arange(1, num_neurons + 1) + step_size = 1 + theta = 4 + neg_tau_ref = 0 + wt = 2 + t_inj_spk = {4: -1, 6: -1, 8: 2} + inj_times = list(t_inj_spk.keys()) + total_period = (theta // step_size) - neg_tau_ref + num_epochs = 5 + num_steps = \ + (theta // step_size) + num_epochs * total_period + inj_times[1] + v_scif, v_lif_wta, v_lif_sig = self.run_test(num_steps=num_steps, + num_neurons=num_neurons, + cost_diag=cost_diag, + step_size=step_size, + theta=theta, + neg_tau_ref=neg_tau_ref, + wt=wt, + t_inj_spk=t_inj_spk) + # voltages = np.hstack((np.arange(num_steps).reshape(num_steps, 1), + # v_scif, + # v_lif_wta, + # v_lif_sig)) + # np.set_printoptions(linewidth=np.inf, threshold=np.inf) + # print(voltages) + # Test pre-inhibitory-injection SCIF voltage and spiking + spk_idxs_pre_inj = np.array([theta // step_size]).astype(int) - 1 + wta_pos_spk_pre_inj = spk_idxs_pre_inj + 1 + sig_pos_spk_pre_inj = wta_pos_spk_pre_inj + 1 + inh_inj = inj_times[0] + wta_spk_rfct_interrupt = inh_inj + 1 + sig_spk_rfct_interrupt = wta_spk_rfct_interrupt + 1 + self.assertTrue(np.all(v_scif[spk_idxs_pre_inj] == neg_tau_ref)) + self.assertTrue(np.all(v_lif_wta[wta_pos_spk_pre_inj] == 1)) + self.assertTrue(np.all(v_lif_sig[sig_pos_spk_pre_inj] == + cost_diag + wt * step_size)) + v_gt_inh_inj = (inh_inj - spk_idxs_pre_inj + 1) - t_inj_spk[inh_inj] + self.assertTrue(np.all(v_scif[inh_inj] == v_gt_inh_inj)) + self.assertTrue(np.all(v_lif_wta[wta_spk_rfct_interrupt] == 1)) + self.assertTrue(np.all(v_lif_sig[sig_spk_rfct_interrupt] == + cost_diag + wt * step_size)) + # Test post-inhibitory-injection SCIF voltage and spiking + spk_idxs_post_inj = np.array([inj_times[2] + (theta // step_size) - 1 + + j * total_period for j in range( + num_epochs)]).astype(int) + wta_pos_spk_idxs = spk_idxs_post_inj + 1 + sig_pos_spk_idxs = wta_pos_spk_idxs + 1 + self.assertTrue(np.all(v_scif[spk_idxs_post_inj] == neg_tau_ref)) + self.assertTrue(np.all(v_lif_wta[wta_pos_spk_idxs] == 1)) + self.assertTrue(np.all(v_lif_sig[sig_pos_spk_idxs] == + cost_diag + wt * step_size)) From 12a317805e183c0725f6d6d6ba2caf7d0d835031 Mon Sep 17 00:00:00 2001 From: "Risbud, Sumedh" Date: Fri, 23 Sep 2022 06:00:12 -0700 Subject: [PATCH 5/5] Minor fixes post review by @phstratmann Signed-off-by: Risbud, Sumedh --- src/lava/proc/scif/models.py | 9 ++++----- tests/lava/proc/scif/test_models.py | 18 ------------------ 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/lava/proc/scif/models.py b/src/lava/proc/scif/models.py index 472912144..c467217c9 100644 --- a/src/lava/proc/scif/models.py +++ b/src/lava/proc/scif/models.py @@ -37,19 +37,18 @@ def __init__(self, proc_params): super(AbstractPyModelScifFixed, self).__init__(proc_params) self.a_in_data = np.zeros(proc_params['shape']) - def _prng(self, shape_like): + def _prng(self, intg_idx_): """Pseudo-random number generator """ # ToDo: Choosing a 16-bit signed random integer. For bit-accuracy, # need to replace it with Loihi-conformant LFSR function - prand = np.zeros_like(shape_like) + prand = np.zeros(shape=(len(intg_idx_),)) if prand.size > 0: rand_nums = \ np.random.randint(-2 ** 15, 2 ** 15 - 1, size=prand.size) # Assign random numbers only to neurons, for which noise is enabled - prand = rand_nums * self.noise_ampl - + prand = rand_nums * self.noise_ampl[intg_idx_] return prand def _update_buffers(self): @@ -71,7 +70,7 @@ def _integration_dynamics(self, intg_idx): spk_hist_to_intg = self.spk_hist[intg_idx] # beta to be integrated step_size_to_intg = self.step_size[intg_idx] # bias to be integrated - lfsr = self._prng(shape_like=state_to_intg) + lfsr = self._prng(intg_idx_=intg_idx) state_to_intg = state_to_intg + lfsr + cnstr_to_intg + step_size_to_intg np.clip(state_to_intg, a_min=0, a_max=2 ** 23 - 1, out=state_to_intg) diff --git a/tests/lava/proc/scif/test_models.py b/tests/lava/proc/scif/test_models.py index 8f64673f6..445aa67d3 100644 --- a/tests/lava/proc/scif/test_models.py +++ b/tests/lava/proc/scif/test_models.py @@ -128,12 +128,6 @@ def test_scif_fixed_pt_no_noise(self) -> None: neg_tau_ref=neg_tau_ref, wt=wt, t_inj_spk={}) - # voltages = np.hstack((np.arange(num_steps).reshape(num_steps, 1), - # v_scif, - # v_lif_wta, - # v_lif_sig)) - # np.set_printoptions(linewidth=np.inf, threshold=np.inf) - # print(voltages) spk_idxs = np.array([theta // step_size - 1 + j * total_period for j in range(num_epochs)]).astype(int) wta_pos_spk_idxs = spk_idxs + 1 @@ -364,12 +358,6 @@ def test_scif_fixed_pt_no_noise(self) -> None: neg_tau_ref=neg_tau_ref, wt=wt, t_inj_spk={}) - # voltages = np.hstack((np.arange(num_steps).reshape(num_steps, 1), - # v_scif, - # v_lif_wta, - # v_lif_sig)) - # np.set_printoptions(linewidth=np.inf, threshold=np.inf) - # print(voltages) spk_idxs = np.array([theta // step_size - 1 + j * total_period for j in range(num_epochs)]).astype(int) wta_pos_spk_idxs = spk_idxs + 1 @@ -410,12 +398,6 @@ def test_scif_fp_no_noise_interrupt_rfct_mid(self) -> None: neg_tau_ref=neg_tau_ref, wt=wt, t_inj_spk=t_inj_spk) - # voltages = np.hstack((np.arange(num_steps).reshape(num_steps, 1), - # v_scif, - # v_lif_wta, - # v_lif_sig)) - # np.set_printoptions(linewidth=np.inf, threshold=np.inf) - # print(voltages) # Test pre-inhibitory-injection SCIF voltage and spiking spk_idxs_pre_inj = np.array([theta // step_size]).astype(int) - 1 wta_pos_spk_pre_inj = spk_idxs_pre_inj + 1