diff --git a/src/lava/frameworks/loihi2.py b/src/lava/frameworks/loihi2.py
new file mode 100644
index 000000000..07007d767
--- /dev/null
+++ b/src/lava/frameworks/loihi2.py
@@ -0,0 +1,14 @@
+# Copyright (C) 2022-23 Intel Corporation
+# SPDX-License-Identifier: BSD-3-Clause
+# See: https://spdx.org/licenses/
+
+from lava.networks.gradedvecnetwork import (InputVec, OutputVec, GradedVec,
+ GradedDense, GradedSparse,
+ ProductVec,
+ LIFVec,
+ NormalizeNet)
+
+from lava.networks.resfire import ResFireVec
+
+from lava.magma.core.run_conditions import RunSteps, RunContinuous
+from lava.magma.core.run_configs import Loihi2SimCfg, Loihi2HwCfg
diff --git a/src/lava/networks/gradedvecnetwork.py b/src/lava/networks/gradedvecnetwork.py
new file mode 100644
index 000000000..abf163b7d
--- /dev/null
+++ b/src/lava/networks/gradedvecnetwork.py
@@ -0,0 +1,324 @@
+# Copyright (C) 2022-23 Intel Corporation
+# SPDX-License-Identifier: BSD-3-Clause
+# See: https://spdx.org/licenses/
+
+import numpy as np
+import typing as ty
+
+from lava.proc.graded.process import InvSqrt
+from lava.proc.graded.process import NormVecDelay
+from lava.proc.sparse.process import Sparse
+from lava.proc.dense.process import Dense
+from lava.proc.prodneuron.process import ProdNeuron
+from lava.proc.graded.process import GradedVec as GradedVecProc
+from lava.proc.lif.process import LIF
+from lava.proc.io import sink, source
+
+from .network import Network, AlgebraicVector, AlgebraicMatrix
+
+
+class InputVec(AlgebraicVector):
+ """InputVec
+ Simple input vector. Adds algebraic syntax to RingBuffer
+
+ Parameters
+ ----------
+ vec : np.ndarray
+ NxM array of input values. Input will repeat every M steps.
+ exp : int, optional
+ Set the fixed point base value
+ loihi2 : bool, optional
+ Flag to create the adapters for loihi 2.
+ """
+
+ def __init__(self,
+ vec: np.ndarray,
+ loihi2: ty.Optional[bool] = False,
+ exp: ty.Optional[int] = 0,
+ **kwargs) -> None:
+
+ self.loihi2 = loihi2
+ self.shape = np.atleast_2d(vec).shape
+ self.exp = exp
+
+ # Convert it to fixed point base
+ vec *= 2**self.exp
+
+ self.inport_plug = source.RingBuffer(data=np.atleast_2d(vec))
+
+ if self.loihi2:
+ from lava.proc import embedded_io as eio
+ self.inport_adapter = eio.spike.PyToNxAdapter(
+ shape=(self.shape[0],),
+ num_message_bits=24)
+ self.inport_plug.s_out.connect(self.inport_adapter.inp)
+ self.out_port = self.inport_adapter.out
+
+ else:
+ self.out_port = self.inport_plug.s_out
+
+ def __lshift__(self, other):
+ # Maybe this could be done with a numpy array and call set_data?
+ return NotImplemented
+
+
+class OutputVec(Network):
+ """OutputVec
+ Records spike output. Adds algebraic syntax to RingBuffer
+
+ Parameters
+ ----------
+ shape : tuple(int)
+ shape of the output to record
+ buffer : int, optional
+ length of the recording.
+ (buffer is overwritten if shorter than sim time).
+ loihi2 : bool, optional
+ Flag to create the adapters for loihi 2.
+ num_message_bits : int
+ size of output message. ("0" is for unary spike event).
+ """
+
+ def __init__(self,
+ shape: ty.Tuple[int, ...],
+ buffer: int = 1,
+ loihi2: ty.Optional[bool] = False,
+ num_message_bits: ty.Optional[int] = 24,
+ **kwargs) -> None:
+
+ self.shape = shape
+ self.buffer = buffer
+ self.loihi2 = loihi2
+ self.num_message_bits = num_message_bits
+
+ self.outport_plug = sink.RingBuffer(
+ shape=self.shape, buffer=self.buffer, **kwargs)
+
+ if self.loihi2:
+ from lava.proc import embedded_io as eio
+ self.outport_adapter = eio.spike.NxToPyAdapter(
+ shape=self.shape, num_message_bits=self.num_message_bits)
+ self.outport_adapter.out.connect(self.outport_plug.a_in)
+ self.in_port = self.outport_adapter.inp
+ else:
+ self.in_port = self.outport_plug.a_in
+
+ def get_data(self):
+ return (self.outport_plug.data.get().astype(np.int32) << 8) >> 8
+
+
+class LIFVec(AlgebraicVector):
+ """LIFVec
+ Network wrapper to LIF neuron.
+
+ Parameters
+ ----------
+ See lava.proc.lif.process.LIF
+ """
+
+ def __init__(self, **kwargs):
+ self.main = LIF(**kwargs)
+
+ self.in_port = self.main.a_in
+ self.out_port = self.main.s_out
+
+
+class GradedVec(AlgebraicVector):
+ """GradedVec
+ Simple graded threshold vector with no dynamics.
+
+ Parameters
+ ----------
+ shape : tuple(int)
+ Number and topology of neurons.
+ vth : int, optional
+ Threshold for spiking.
+ exp : int, optional
+ Fixed point base of the vector.
+ """
+
+ def __init__(self,
+ shape: ty.Tuple[int, ...],
+ vth: int = 10,
+ exp: int = 0,
+ **kwargs):
+
+ self.shape = shape
+ self.vth = vth
+ self.exp = exp
+
+ self.main = GradedVecProc(shape=self.shape, vth=self.vth, exp=self.exp)
+ self.in_port = self.main.a_in
+ self.out_port = self.main.s_out
+
+ super().__init__()
+
+ def __mul__(self, other):
+ if isinstance(other, GradedVec):
+ # Create the product network
+ prod_layer = ProductVec(shape=self.shape, vth=1, exp=self.exp)
+
+ weightsI = np.eye(self.shape[0])
+
+ weights_A = GradedSparse(weights=weightsI)
+ weights_B = GradedSparse(weights=weightsI)
+ weights_out = GradedSparse(weights=weightsI)
+
+ prod_layer << (weights_A @ self, weights_B @ other)
+ weights_out @ prod_layer
+ return weights_out
+ else:
+ return NotImplemented
+
+
+class ProductVec(AlgebraicVector):
+ """ProductVec
+
+ Neuron that will multiply values on two input channels.
+
+ Parameters
+ ----------
+ shape : tuple(int)
+ Number and topology of neurons.
+ vth : int
+ Threshold for spiking.
+ exp : int
+ Fixed point base of the vector.
+ """
+
+ def __init__(self,
+ shape: ty.Tuple[int, ...],
+ vth: ty.Optional[int] = 10,
+ exp: ty.Optional[int] = 0,
+ **kwargs):
+ self.shape = shape
+ self.vth = vth
+ self.exp = exp
+
+ self.main = ProdNeuron(shape=self.shape, vth=self.vth, exp=self.exp)
+
+ self.in_port = self.main.a_in1
+ self.in_port2 = self.main.a_in2
+
+ self.out_port = self.main.s_out
+
+ def __lshift__(self, other):
+ # We're going to override the behavior here,
+ # since there are two ports the API idea is:
+ # prod_layer << (conn1, conn2)
+ if isinstance(other, (list, tuple)):
+ # It should be only length 2, and a Network object,
+ # TODO: add checks
+ other[0].out_port.connect(self.in_port)
+ other[1].out_port.connect(self.in_port2)
+ else:
+ return NotImplemented
+
+
+class GradedDense(AlgebraicMatrix):
+ """GradedDense
+ Network wrapper for Dense. Adds algebraic syntax to Dense.
+
+ Parameters
+ ----------
+ See lava.proc.dense.process.Dense
+
+ weights : numpy.ndarray
+ Weight matrix expressed as floating point. Weights will be automatically
+ reconfigured to fixed point (may lead to changes due to rounding).
+ exp : int, optional
+ Fixed point base of the weight (reconfigures weights/weight_exp).
+ """
+
+ def __init__(self,
+ weights: np.ndarray,
+ exp: int = 7,
+ **kwargs):
+ self.exp = exp
+
+ # Adjust the weights to the fixed point
+ w = weights * 2 ** self.exp
+
+ self.main = Dense(weights=w,
+ num_message_bits=24,
+ num_weight_bits=8,
+ weight_exp=-self.exp)
+
+ self.in_port = self.main.s_in
+ self.out_port = self.main.a_out
+
+
+class GradedSparse(AlgebraicMatrix):
+ """GradedSparse
+ Network wrapper for Sparse. Adds algebraic syntax to Sparse.
+
+ Parameters
+ ----------
+ See lava.proc.sparse.process.Sparse
+
+ weights : numpy.ndarray
+ Weight matrix expressed as floating point. Weights will be automatically
+ reconfigured to fixed point (may lead to changes due to rounding).
+ exp : int, optional
+ Fixed point base of the weight (reconfigures weights/weight_exp).
+ """
+
+ def __init__(self,
+ weights: np.ndarray,
+ exp: int = 7,
+ **kwargs):
+
+ self.exp = exp
+
+ # Adjust the weights to the fixed point
+ w = weights * 2 ** self.exp
+ self.main = Sparse(weights=w,
+ num_message_bits=24,
+ num_weight_bits=8,
+ weight_exp=-self.exp)
+
+ self.in_port = self.main.s_in
+ self.out_port = self.main.a_out
+
+
+class NormalizeNet(AlgebraicVector):
+ """NormalizeNet
+ Creates a layer for normalizing vector inputs
+
+ Parameters
+ ----------
+ shape : tuple(int)
+ Number and topology of neurons.
+ exp : int
+ Fixed point base of the vector.
+ """
+
+ def __init__(self,
+ shape: ty.Tuple[int, ...],
+ exp: ty.Optional[int] = 12,
+ **kwargs):
+ self.shape = shape
+ self.fpb = exp
+
+ vec_to_fpinv_w = np.ones((1, self.shape[0]))
+ fpinv_to_vec_w = np.ones((self.shape[0], 1))
+ weight_exp = 0
+
+ self.vfp_dense = Dense(weights=vec_to_fpinv_w,
+ num_message_bits=24,
+ weight_exp=-weight_exp)
+ self.fpv_dense = Dense(weights=fpinv_to_vec_w,
+ num_message_bits=24,
+ weight_exp=-weight_exp)
+
+ self.main = NormVecDelay(shape=self.shape, vth=1,
+ exp=self.fpb)
+ self.fp_inv_neuron = InvSqrt(shape=(1,), fp_base=self.fpb)
+
+ self.main.s2_out.connect(self.vfp_dense.s_in)
+ self.vfp_dense.a_out.connect(self.fp_inv_neuron.a_in)
+ self.fp_inv_neuron.s_out.connect(self.fpv_dense.s_in)
+ self.fpv_dense.a_out.connect(self.main.a_in2)
+
+ self.in_port = self.main.a_in1
+ self.out_port = self.main.s_out
diff --git a/src/lava/networks/network.py b/src/lava/networks/network.py
new file mode 100644
index 000000000..8e9d1e5e1
--- /dev/null
+++ b/src/lava/networks/network.py
@@ -0,0 +1,154 @@
+# Copyright (C) 2022-23 Intel Corporation
+# SPDX-License-Identifier: BSD-3-Clause
+# See: https://spdx.org/licenses/
+
+import numpy as np
+import typing as ty
+from scipy.sparse import csr_matrix
+
+from lava.magma.core.process.ports.ports import InPort, OutPort
+from lava.magma.core.process.process import AbstractProcess
+
+
+class NetworkList(list):
+ """NetworkList
+ This is a list subclass to keep track of Network objects that
+ are added using the '+' operator.
+ """
+
+ def __init__(self, iterable):
+ super().__init__(iterable)
+
+
+class Network:
+ """Network
+ Abstract Network object.
+
+ Networks contain other networks and lava processes.
+ """
+
+ in_port: InPort
+ out_port: OutPort
+ main: AbstractProcess
+
+ def run(self, **kwargs):
+ self.main.run(**kwargs)
+
+ def stop(self, **kwargs):
+ self.main.stop(**kwargs)
+
+ def __lshift__(self,
+ other):
+ # Self-referential type hint is causing a NameError
+ # other: ty.Union[Network, NetworkList]):
+ """
+ Operator overload of "<<" to connect Network objects.
+
+ EPF: note that the precedence could matter if we include more
+ operators. We want this assignment operator to have lowest
+ precedence, which "<<" is lower than "+", so it works. However, it
+ is higher than i.e. "^" which would not work. Comparisons have even
+ lower precedence, "<=" could be better.
+ """
+ if isinstance(other, Network):
+ other.out_port.connect(self.in_port)
+ return self
+ elif isinstance(other, NetworkList):
+ for o in other:
+ self << o
+ return self
+ else:
+ return NotImplemented
+
+ def __add__(self,
+ other):
+ # Self-referential typing is causing a NameError
+ # other: ty.Union[Network, NetworkList]):
+ """
+ Operator overload of "+" to act as summation in algebraic syntax.
+ """
+ if isinstance(other, Network):
+ return NetworkList([self, other])
+ elif isinstance(other, NetworkList):
+ other.append(self)
+ return other
+ else:
+ return NotImplemented
+ # When chaining operations this is used for [weights1, weights2] + weights3
+ __radd__ = __add__
+
+
+class AlgebraicVector(Network):
+ """AlgebraicVector
+ Provides vector operator syntax for Networks.
+ """
+
+ def __lshift__(self,
+ other):
+ # Self-referential typing is causing a NameError
+ # other: ty.Union[AlgebraicVector, Network, NetworkList]):
+ """
+ Operator overload of "<<" to connect AlgebraicVector objects.
+ """
+
+ if isinstance(other, AlgebraicVector):
+ # If a vector is connected to another vector, an Identity
+ # connection is generated and the two procs are connected.
+
+ # This import statement needs to be here to avoid a circular
+ # import error
+ from lava.networks.gradedvecnetwork import GradedSparse
+ weightsI = csr_matrix(np.eye(np.prod(self.shape)))
+ I_syn = GradedSparse(weights=weightsI)
+ other.out_port.connect(I_syn.in_port)
+ I_syn.out_port.connect(self.in_port)
+ return self
+
+ elif isinstance(other, Network):
+ # This will take care of the standard weights to neurons.
+ other.out_port.connect(self.in_port)
+ return self
+ elif isinstance(other, NetworkList):
+ # When using the plus operator to add
+ for o in other:
+ self << o
+ return self
+ else:
+ return NotImplemented
+
+
+class AlgebraicMatrix(Network):
+ """AlgebraicMatrix
+ Provides matrix operator syntax for Networks.
+ """
+
+ def __matmul__(self,
+ other):
+ # Self-referential typing is causing a NameError
+ # other: AlgebraicVector):
+ """
+ Operator overload of "@" to form matrix-vector product.
+ """
+ if isinstance(other, AlgebraicVector):
+ other.out_port.connect(self.in_port)
+ return self
+ else:
+ return NotImplemented
+
+ def __mul__(self,
+ other):
+ # Self-referential typing is causing a NameError
+ # other: AlgebraicMatrix):
+ """
+ Operator overload of "*" to for multiplication.
+ """
+ if isinstance(other, AlgebraicMatrix):
+ from lava.networks.gradedvecnetwork import ProductVec
+ # How to pass in exp?
+ prod_layer = ProductVec(shape=self.shape, vth=1, exp=0)
+
+ prod_layer << (self, other)
+
+ return prod_layer
+ else:
+ return NotImplemented
diff --git a/src/lava/networks/resfire.py b/src/lava/networks/resfire.py
new file mode 100644
index 000000000..7712a4649
--- /dev/null
+++ b/src/lava/networks/resfire.py
@@ -0,0 +1,45 @@
+# Copyright (C) 2022-23 Intel Corporation
+# SPDX-License-Identifier: BSD-3-Clause
+# See: https://spdx.org/licenses/
+
+import numpy as np
+from .network import AlgebraicVector
+from lava.proc.resfire.process import RFZero
+
+
+class ResFireVec(AlgebraicVector):
+ """
+ Network wrapper for resonate-and-fire neurons
+ """
+
+ def __init__(self, **kwargs):
+ self.uth = kwargs.pop('uth', 10)
+ self.shape = kwargs.pop('shape', (1,))
+ self.freqs = kwargs.pop('freqs', np.array([10]))
+ self.decay_tau = kwargs.pop('decay_tau', np.array([1]))
+ self.dt = kwargs.pop('dt', 0.001)
+
+ self.freqs = np.array(self.freqs)
+ self.decay_tau = np.array(self.decay_tau)
+
+ self.main = RFZero(shape=self.shape, uth=self.uth,
+ freqs=self.freqs, decay_tau=self.decay_tau,
+ dt=self.dt)
+
+ self.in_port = self.main.u_in
+ self.in_port2 = self.main.v_in
+
+ self.out_port = self.main.s_out
+
+ def __lshift__(self, other):
+ # We're going to override the behavior here
+ # since theres two ports the API idea is:
+ # rf_layer << (conn1, conn2)
+ if isinstance(other, (list, tuple)):
+ # it should be only length 2, and a Network object,
+ # add checks
+ other[0].out_port.connect(self.in_port)
+ other[1].out_port.connect(self.in_port2)
+ else:
+ # in this case we will just connect to in_port
+ super().__lshift__(other)
diff --git a/src/lava/proc/graded/models.py b/src/lava/proc/graded/models.py
index b87f925a9..969caa1d4 100644
--- a/src/lava/proc/graded/models.py
+++ b/src/lava/proc/graded/models.py
@@ -92,7 +92,7 @@ def run_spk(self) -> None:
@implements(proc=InvSqrt, protocol=LoihiProtocol)
@requires(CPU)
-@tag('float')
+@tag('floating_pt')
class InvSqrtModelFloat(PyLoihiProcessModel):
"""Implementation of InvSqrt in floating point"""
a_in = LavaPyType(PyInPort.VEC_DENSE, float)
@@ -111,9 +111,19 @@ def run_spk(self) -> None:
self.s_out.send(sp_out)
-def make_fpinv_table(fp_base):
+def make_fpinv_table(fp_base: int) -> np.ndarray:
"""
- Creates the table for fp inverse algorithm.
+ Creates the table for fp inverse square root algorithm.
+
+ Parameters
+ ----------
+ fp_base : int
+ Base of the fixed point.
+
+ Returns
+ -------
+ Y_est : np.ndarray
+ Initialization look-up table for fp inverse square root.
"""
n_bits = 24
B = 2**fp_base
@@ -121,22 +131,49 @@ def make_fpinv_table(fp_base):
Y_est = np.zeros((n_bits), dtype='int')
n_adj = 1.238982962
- for m in range(n_bits): # span the 24 bits, negate the decimal base
+ for m in range(n_bits): # Span the 24 bits, negate the decimal base
Y_est[n_bits - m - 1] = 2 * int(B / (2**((m - fp_base) / 2) * n_adj))
return Y_est
-def clz(val):
+def clz(val: int) -> int:
"""
Count lead zeros.
+
+ Parameters
+ ----------
+ val : int
+ Integer value for counting lead zeros.
+
+ Returns
+ -------
+ out_val : int
+ Number of leading zeros.
"""
- return (24 - (int(np.log2(val)) + 1))
+ out_val = (24 - (int(np.log2(val)) + 1))
+ return out_val
-def inv_sqrt(s_fp, n_iters=5, b_fraction=12):
+def inv_sqrt(s_fp: int,
+ n_iters: int = 5,
+ b_fraction: int = 12) -> int:
"""
- Runs the fixed point inverse square root algorithm
+ Runs the fixed point inverse square root algorithm.
+
+ Parameters
+ ----------
+ s_fp : int
+ Fixed point value to calulate inverse square root.
+ n_iters : int, optional
+ Number of iterations for fixed point inverse square root algorithm.
+ b_fraction : int, optional
+ Fixed point base.
+
+ Returns
+ -------
+ y_i : int
+ Approximate inverse square root in fixed point.
"""
Y_est = make_fpinv_table(b_fraction)
m = clz(s_fp)
diff --git a/src/lava/proc/graded/process.py b/src/lava/proc/graded/process.py
index 6c5943a46..ed7c41b3f 100644
--- a/src/lava/proc/graded/process.py
+++ b/src/lava/proc/graded/process.py
@@ -10,12 +10,23 @@
from lava.magma.core.process.ports.ports import InPort, OutPort
-def loihi2round(vv):
+def loihi2round(vv: np.ndarray) -> np.ndarray:
"""
Round values in numpy array the way loihi 2
performs rounding/truncation.
+
+ Parameters
+ ----------
+ vv : np.ndarray
+ Input values to be rounded consistent with loihi2 rouding.
+
+ Returns
+ -------
+ vv_r : np.ndarray
+ Output values rounded consistent with loihi2 rouding.
"""
- return np.fix(vv + (vv > 0) - 0.5).astype('int')
+ vv_r = np.fix(vv + (vv > 0) - 0.5).astype('int')
+ return vv_r
class GradedVec(AbstractProcess):
@@ -28,12 +39,12 @@ class GradedVec(AbstractProcess):
Parameters
----------
- shape: tuple(int)
- number and topology of neurons
- vth: int
- threshold for spiking
- exp: int
- fixed point base
+ shape : tuple(int)
+ Number and topology of neurons.
+ vth : int
+ Threshold for spiking.
+ exp : int
+ Fixed point base.
"""
def __init__(
@@ -78,12 +89,12 @@ class NormVecDelay(AbstractProcess):
Parameters
----------
- shape: tuple(int)
- number and topology of neurons
- vth: int
- threshold for spiking
- exp: int
- fixed point base
+ shape : tuple(int)
+ Number and topology of neurons.
+ vth : int
+ Threshold for spiking.
+ exp : int
+ Fixed point base.
"""
def __init__(
@@ -123,8 +134,11 @@ class InvSqrt(AbstractProcess):
Parameters
----------
+ shape : tuple(int)
+ Number and topology of neurons.
+
fp_base : int
- Base of the fixed-point representation
+ Base of the fixed-point representation.
"""
def __init__(
@@ -133,7 +147,7 @@ def __init__(
fp_base: ty.Optional[int] = 12) -> None:
super().__init__(shape=shape)
- # base of the decimal point
+ # Base of the decimal point
self.fp_base = Var(shape=(1,), init=fp_base)
self.a_in = InPort(shape=shape)
self.s_out = OutPort(shape=shape)
diff --git a/src/lava/proc/prodneuron/process.py b/src/lava/proc/prodneuron/process.py
index 417ed5490..94d1a10c9 100644
--- a/src/lava/proc/prodneuron/process.py
+++ b/src/lava/proc/prodneuron/process.py
@@ -20,7 +20,7 @@ def __init__(
Multiplies two graded inputs and outputs result as graded spike.
v[t] = (a_in1 * a_in2) >> exp
- s_out = v[t] * (v[t] > vth)
+ s_out = v[t] * (|v[t]| > vth)
Parameters
----------
diff --git a/tests/lava/frameworks/__init__.py b/tests/lava/frameworks/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/lava/frameworks/test_frameworks.py b/tests/lava/frameworks/test_frameworks.py
new file mode 100644
index 000000000..e833995c3
--- /dev/null
+++ b/tests/lava/frameworks/test_frameworks.py
@@ -0,0 +1,17 @@
+# Copyright (C) 2023 Intel Corporation
+# SPDX-License-Identifier: BSD-3-Clause
+# See: https://spdx.org/licenses/
+
+import unittest
+
+
+class TestFrameworks(unittest.TestCase):
+ """Tests for framework import."""
+
+ def test_frameworks_loihi2_import(self):
+ """Tests if framework import fails."""
+ import lava.frameworks.loihi2 as lv
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/lava/networks/__init__.py b/tests/lava/networks/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/lava/networks/test_networks.py b/tests/lava/networks/test_networks.py
new file mode 100644
index 000000000..317977c92
--- /dev/null
+++ b/tests/lava/networks/test_networks.py
@@ -0,0 +1,28 @@
+# Copyright (C) 2023 Intel Corporation
+# SPDX-License-Identifier: BSD-3-Clause
+# See: https://spdx.org/licenses/
+
+import unittest
+import numpy as np
+from scipy.sparse import csr_matrix
+
+import lava.frameworks.loihi2 as lv
+
+
+class TestNetworks(unittest.TestCase):
+ """Tests for LVA Networks."""
+
+ def test_networks_instantiate(self):
+ """Tests if LVA Networks can be instantiated."""
+ inputvec = lv.InputVec(np.ones((1,)), shape=(1,))
+ outputvec = lv.OutputVec(shape=(1,), buffer=1)
+ threshvec = lv.GradedVec(shape=(1,))
+ gradeddense = lv.GradedDense(weights=np.ones((1, 1)))
+ gradedsparse = lv.GradedSparse(weights=csr_matrix(np.ones((1, 1))))
+ productvec = lv.ProductVec(shape=(1,))
+ lifvec = lv.LIFVec(shape=(1,))
+ normnet = lv.NormalizeNet(shape=(1,))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/lava/proc/graded/test_graded.py b/tests/lava/proc/graded/test_graded.py
index b4c4b5027..b2acbe882 100644
--- a/tests/lava/proc/graded/test_graded.py
+++ b/tests/lava/proc/graded/test_graded.py
@@ -17,10 +17,10 @@
class TestGradedVecProc(unittest.TestCase):
- """Tests for GradedVec"""
+ """Tests for GradedVec."""
def test_gradedvec_dot_dense(self):
- """Tests that GradedVec and Dense computes dot product"""
+ """Tests that GradedVec and Dense computes dot product."""
num_steps = 10
v_thresh = 1
@@ -59,7 +59,7 @@ def test_gradedvec_dot_dense(self):
self.assertTrue(np.all(out_data[:, (3, 7)] == expected_out[:, (2, 6)]))
def test_gradedvec_dot_sparse(self):
- """Tests that GradedVec and Dense computes dot product"""
+ """Tests that GradedVec and Dense computes dot product."""
num_steps = 10
v_thresh = 1
@@ -103,8 +103,8 @@ class TestInvSqrtProc(unittest.TestCase):
"""Tests for inverse square process."""
def test_invsqrt_calc(self):
- """Checks the InvSqrt calculation"""
- fp_base = 12 # base of the decimal point
+ """Checks the InvSqrt calculation."""
+ fp_base = 12 # Base of the decimal point
num_steps = 25
weights1 = np.zeros((1, 1))
@@ -147,10 +147,10 @@ def test_invsqrt_calc(self):
class TestNormVecDelayProc(unittest.TestCase):
- """Tests for NormVecDelay"""
+ """Tests for NormVecDelay."""
def test_norm_vec_delay_out1(self):
- """Checks the first channel output of NormVecDelay"""
+ """Checks the first channel output of NormVecDelay."""
weight_exp = 7
num_steps = 10
@@ -203,17 +203,19 @@ def test_norm_vec_delay_out1(self):
ch1 = (weights1 @ inp_data1) / 2**weight_exp
ch2 = (weights2 @ inp_data2) / 2**weight_exp
- # I'm using roll to account for the two step delay but hacky
- # be careful if inputs change
- # hmm.. there seems to be a delay step missing compared to
+ # I'm using roll to account for the two step delay in NormVecDelay.
+ # However, this is a hack, as the inputs need to be 0 at the end
+ # of the simulation, since roll wraps the values.
+ # Be wary that this potentially won't be correct with different inputs.
+ # There seems to be a delay step missing compared to
# ncmodel, not sure where the delay should go...
expected_out = np.roll(ch1, 1) * ch2
- # Then there is one extra timestep from hardware
+ # Then there is one extra delay timestep from hardware
self.assertTrue(np.all(expected_out[:, :-1] == out_data[:, 1:]))
def test_norm_vec_delay_out2(self):
- """Checks the second channel output of NormVecDelay"""
+ """Checks the second channel output of NormVecDelay."""
weight_exp = 7
num_steps = 10
@@ -265,7 +267,7 @@ def test_norm_vec_delay_out2(self):
ch1 = (weights1 @ inp_data1) / 2**weight_exp
expected_out = ch1 ** 2
- # then there is one extra timestep from hardware
+ # Then there is one extra timestep from hardware
self.assertTrue(np.all(expected_out[:, :-1] == out_data[:, 1:]))
diff --git a/tests/lava/proc/prodneuron/test_prod_neuron.py b/tests/lava/proc/prodneuron/test_prod_neuron.py
index 13919d3b9..65eb2bf2e 100644
--- a/tests/lava/proc/prodneuron/test_prod_neuron.py
+++ b/tests/lava/proc/prodneuron/test_prod_neuron.py
@@ -14,7 +14,10 @@
class TestProdNeuronProc(unittest.TestCase):
+ """Tests for ProdNeuron."""
+
def test_prod_neuron_out(self):
+ """Tests prod neuron calcultion is correct."""
weight_exp = 7
num_steps = 10
@@ -68,7 +71,7 @@ def test_prod_neuron_out(self):
ch2 = (weights2 @ inp_data2) / 2**weight_exp
expected_out = ch1 * ch2
- # then there is one extra timestep from hardware
+ # Then there is one extra timestep from hardware
self.assertTrue(np.all(expected_out[:, :-1] == out_data[:, 1:]))
diff --git a/tests/lava/tutorials/test_tutorials-lva.py b/tests/lava/tutorials/test_tutorials-lva.py
new file mode 100644
index 000000000..7234c5de4
--- /dev/null
+++ b/tests/lava/tutorials/test_tutorials-lva.py
@@ -0,0 +1,222 @@
+# Copyright (C) 2022-2024 Intel Corporation
+# SPDX-License-Identifier: BSD-3-Clause
+# See: https://spdx.org/licenses/
+
+import glob
+import os
+import platform
+import subprocess # noqa S404
+import sys
+import tempfile
+import typing as ty
+import unittest
+from test import support
+
+import lava
+import nbformat
+
+import tutorials
+
+
+class TestTutorials(unittest.TestCase):
+ """Export notebook, execute to check for errors."""
+
+ system_name = platform.system().lower()
+
+ def _execute_notebook(
+ self, base_dir: str, path: str
+ ) -> ty.Tuple[ty.Type[nbformat.NotebookNode], ty.List[str]]:
+ """Execute a notebook via nbconvert and collect output.
+
+ Parameters
+ ----------
+ base_dir : str
+ notebook search directory
+ path : str
+ path to notebook
+
+ Returns
+ -------
+ Tuple
+ (parsed nbformat.NotebookNode object, list of execution errors)
+ """
+
+ cwd = os.getcwd()
+ dir_name, notebook = os.path.split(path)
+ try:
+ env = self._update_pythonpath(base_dir, dir_name)
+ nb = self._convert_and_execute_notebook(notebook, env)
+ errors = self._collect_errors_from_all_cells(nb)
+ except Exception as e:
+ nb = None
+ errors = str(e)
+ finally:
+ os.chdir(cwd)
+
+ return nb, errors
+
+ def _update_pythonpath(
+ self, base_dir: str, dir_name: str
+ ) -> ty.Dict[str, str]:
+ """Update PYTHONPATH with notebook location.
+
+ Parameters
+ ----------
+ base_dir : str
+ Parent directory to use
+ dir_name : str
+ Directory containing notebook
+
+ Returns
+ -------
+ env : dict
+ Updated dictionary of environment variables
+ """
+ os.chdir(base_dir + "/" + dir_name)
+
+ env = os.environ.copy()
+ module_path = [lava.__path__.__dict__["_path"][0]]
+
+ module_path.extend(
+ [os.path.dirname(module_path[0]), env.get("PYTHONPATH", "")]
+ )
+
+ sys_path = ":".join(map(str, sys.path))
+ env_path = env.get("PYTHONPATH", "")
+ mod_path = ":".join(map(str, module_path))
+
+ env["PYTHONPATH"] = env_path + ":" + mod_path + ":" + sys_path
+
+ return env
+
+ def _convert_and_execute_notebook(
+ self, notebook: str, env: ty.Dict[str, str]
+ ) -> ty.Type[nbformat.NotebookNode]:
+ """Covert notebook and execute it.
+
+ Parameters
+ ----------
+ notebook : str
+ Notebook name
+ env : dict
+ Dictionary of environment variables
+
+ Returns
+ -------
+ nb : nbformat.NotebookNode
+ Notebook dict-like node with attribute-access
+ """
+ with tempfile.NamedTemporaryFile(mode="w+t", suffix=".ipynb") as fout:
+ args = [
+ "jupyter",
+ "nbconvert",
+ "--to",
+ "notebook",
+ "--execute",
+ "--ExecutePreprocessor.timeout=-1",
+ "--output",
+ fout.name,
+ notebook,
+ ]
+ subprocess.check_call(args, env=env) # nosec # noqa: S603
+
+ fout.seek(0)
+ return nbformat.read(fout, nbformat.current_nbformat)
+
+ def _collect_errors_from_all_cells(
+ self, nb: nbformat.NotebookNode
+ ) -> ty.List[str]:
+ """Collect errors from executed notebook.
+
+ Parameters
+ ----------
+ nb : nbformat.NotebookNode
+ Notebook to search for errors
+
+ Returns
+ -------
+ List
+ Collection of errors
+ """
+ errors = []
+ for cell in nb.cells:
+ if "outputs" in cell:
+ for output in cell["outputs"]:
+ if output.output_type == "error":
+ errors.append(output)
+ return errors
+
+ def _run_notebook(self, notebook: str):
+ """Run a specific notebook
+
+ Parameters
+ ----------
+ notebook : str
+ name of notebook to run
+ """
+ cwd = os.getcwd()
+ tutorials_temp_directory = tutorials.__path__.__dict__["_path"][0]
+ tutorials_directory = ""
+
+ tutorials_directory = os.path.realpath(tutorials_temp_directory)
+ os.chdir(tutorials_directory)
+
+ errors_record = {}
+
+ try:
+ glob_pattern = "**/{}".format(notebook)
+ discovered_notebooks = sorted(
+ glob.glob(glob_pattern, recursive=True)
+ )
+
+ self.assertTrue(
+ len(discovered_notebooks) != 0,
+ "Notebook not found. Input to function {}".format(notebook),
+ )
+
+ # If the notebook is found execute it and store any errors
+ for notebook_name in discovered_notebooks:
+ nb, errors = self._execute_notebook(
+ str(tutorials_directory), notebook_name
+ )
+ errors_joined = (
+ "\n".join(errors) if isinstance(errors, list) else errors
+ )
+ if errors:
+ errors_record[notebook_name] = (errors_joined, nb)
+
+ self.assertFalse(
+ errors_record,
+ "Failed to execute Jupyter Notebooks \
+ with errors: \n {}".format(
+ errors_record
+ ),
+ )
+ finally:
+ os.chdir(cwd)
+
+ @unittest.skipIf(system_name != "linux", "Tests work on linux")
+ def test_lava_va_01(self):
+ """Test tutorial lava va 01, Fixed point dot product."""
+ self._run_notebook("lava_va/Tutorial01-Fixed_point_dot_product.ipynb")
+
+ @unittest.skipIf(system_name != "linux", "Tests work on linux")
+ def test_lava_va_02(self):
+ """Test tutorial lava va 02, Fixed point element-wise product."""
+ self._run_notebook(
+ "lava_va/"
+ + "Tutorial02-Fixed_point_elementwise_product.ipynb")
+
+ @unittest.skipIf(system_name != "linux", "Tests work on linux")
+ def test_lava_va_03(self):
+ """Test tutorial lava va 03, Normalization Network."""
+ self._run_notebook("lava_va/Tutorial03-Normalization_network.ipynb")
+
+ @unittest.skipIf(system_name != "linux", "Tests work on linux")
+ def test_lava_va_04(self):
+ """Test tutorial lava va 04, Creating network motifs."""
+ self._run_notebook("lava_va/Tutorial04-Creating_network_motifs.ipynb")
+
+
+if __name__ == "__main__":
+ support.run_unittest(TestTutorials)
diff --git a/tutorials/lava_va/Tutorial01-Fixed_point_dot_product.ipynb b/tutorials/lava_va/Tutorial01-Fixed_point_dot_product.ipynb
new file mode 100644
index 000000000..fbed7bd78
--- /dev/null
+++ b/tutorials/lava_va/Tutorial01-Fixed_point_dot_product.ipynb
@@ -0,0 +1,672 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "3cd2e32f",
+ "metadata": {},
+ "source": [
+ "*Copyright (C) 2022-23 Intel Corporation*
\n",
+ "*SPDX-License-Identifier: BSD-3-Clause*
\n",
+ "*See: https://spdx.org/licenses/*\n",
+ "\n",
+ "---\n",
+ "\n",
+ "# Tutorial 1: An Introduction to Graded Spikes and Fixed-point computations\n",
+ "\n",
+ "**Motivation:** In this tutorial, we will discuss the basics of Lava vector algebra API and computing with graded spikes on Loihi 2. This tutorial will demonstrate simple dot-product matrix operations using graded spikes.\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "72b82564",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pylab import *"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "816aceb0",
+ "metadata": {},
+ "source": [
+ "Lava-VA includes a new set of processes that are compatible with Loihi 2. \n",
+ "\n",
+ "First, we can import some of the standard library using an import package. These are designed to make importing the standard libraries more simple and accessible.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "af469544",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import lava.frameworks.loihi2 as lv"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3a39c84c",
+ "metadata": {},
+ "source": [
+ "Next, we'll get access to Loihi 2, or we can use the CPU backend."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "cf6e92dd",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Running on Loihi 2\n"
+ ]
+ }
+ ],
+ "source": [
+ "from lava.utils import loihi\n",
+ "\n",
+ "loihi.use_slurm_host(loihi_gen=loihi.ChipGeneration.N3B3)\n",
+ "use_loihi2 = loihi.is_installed()\n",
+ "\n",
+ "if use_loihi2:\n",
+ " run_cfg = lv.Loihi2HwCfg()\n",
+ " print(\"Running on Loihi 2\")\n",
+ "else:\n",
+ " run_cfg = lv.Loihi2SimCfg(select_tag='fixed_pt')\n",
+ " print(\"Loihi2 compiler is not available in this system. \"\n",
+ " \"This tutorial will execute on CPU backend.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d2816267",
+ "metadata": {},
+ "source": [
+ "Now, lets setup some inputs, and create the structure for our Loihi 2 algorithm. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "9c3c5a76",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "vec = np.array([40, 30, 20, 10])\n",
+ "weights = np.zeros((3,4))\n",
+ "weights[:, 0] = [8, 9, -7]\n",
+ "weights[:, 1] = [9, 8, -5]\n",
+ "weights[:, 2] = [8, -10, -4]\n",
+ "weights[:, 3] = [8, -10, -3]\n",
+ "\n",
+ "# Note: we define the weights using floating points,\n",
+ "# this will create the equivalent fixed-point \n",
+ "# representation on Loihi 2. We use the weight_exp to \n",
+ "# set the dynamic range. The dynamic range is:\n",
+ "# weight_exp = 8 -- [-1, 1)\n",
+ "# weight_exp = 7 -- [-2, 2)\n",
+ "# weight_exp = 6 -- [-4, 4)\n",
+ "# ...\n",
+ "# weight_exp = 1 -- [-128, 128)\n",
+ "weights /= 10\n",
+ "weight_exp = 7"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "48b567d1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "num_steps=16"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "09c97cca",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "inp_data = np.zeros((vec.shape[0], num_steps))\n",
+ "inp_data[:, 1] = vec.ravel()\n",
+ "inp_data[:, 3] = 4*vec.ravel()\n",
+ "inp_data[:, 5] = 16*vec.ravel()\n",
+ "inp_data[:, 7] = 64*vec.ravel()\n",
+ "inp_data[:, 9] = 256*vec.ravel()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "41d911d8",
+ "metadata": {},
+ "source": [
+ "In this case, I have created an input vector and some weights, and then I will send the input vector in with different magnitudes at different timesteps."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e88436b4",
+ "metadata": {},
+ "source": [
+ "Next, we use the standard library to create the input layer, the synaptic weights, the neuron layer, and the readout layer."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "7590a9fc",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "invec = lv.InputVec(inp_data, loihi2=use_loihi2)\n",
+ "\n",
+ "in_out_syn = lv.GradedDense(weights=weights, exp=weight_exp)\n",
+ "\n",
+ "outvec = lv.GradedVec(shape=(weights.shape[0],), vth=1)\n",
+ "\n",
+ "out_monitor = lv.OutputVec(shape=outvec.shape, buffer=num_steps, loihi2=use_loihi2)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "129d701b",
+ "metadata": {},
+ "source": [
+ "There is a new interface that includes the ability to incorporate operator overloading. This allows constructions of Networks based on an algebraic syntax.\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "97e62916",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "outvec << in_out_syn @ invec\n",
+ "out_monitor << outvec"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a7b70c79",
+ "metadata": {},
+ "source": [
+ "Now we can run the network."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "6b2c0862",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "try:\n",
+ " outvec.run(condition=lv.RunSteps(num_steps=num_steps),\n",
+ " run_cfg=run_cfg)\n",
+ " out_data = out_monitor.get_data()\n",
+ "finally:\n",
+ " outvec.stop()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0668d3be",
+ "metadata": {},
+ "source": [
+ "What we should see is the dot product of the input vector. Since we incremented the input strength, the entire vector output will also grow proportionally."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "c3cad817",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([[ 83, 331, 1321, 5280, 21120],\n",
+ " [ 30, 119, 473, 1890, 7560],\n",
+ " [ -54, -218, -868, -3470, -13880]], dtype=int32)"
+ ]
+ },
+ "execution_count": 10,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "out_data[:,2:11:2]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "552f017a",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([ 83., 30., -54.])"
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "weights @ vec"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c53991fb",
+ "metadata": {},
+ "source": [
+ "There may be some rounding differences due to the rounding of the values, but we see the correct values compared to the numpy calculation."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a93b4a44",
+ "metadata": {},
+ "source": [
+ "## Addition operator overload\n",
+ "\n",
+ "As a second example we will create two weight matrices and show how the additionn operator overload can be used.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "dd826827",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Defining two input streams\n",
+ "vec = np.array([40, 30, 20, 10])\n",
+ "weights = np.zeros((3,4))\n",
+ "weights[:, 0] = [8, 9, -7]\n",
+ "weights[:, 1] = [9, 8, -5]\n",
+ "weights[:, 2] = [8, -10, -4]\n",
+ "weights[:, 3] = [8, -10, -3]\n",
+ "\n",
+ "vec2 = np.array([50, -50, 20, -20])\n",
+ "weights2 = np.zeros((3,4))\n",
+ "weights2[:, 0] = [3, -5, 4]\n",
+ "weights2[:, 1] = [0, -2, -10]\n",
+ "weights2[:, 2] = [6, 8, -4]\n",
+ "weights2[:, 3] = [-5, 7, -7]\n",
+ "\n",
+ "weights /= 10\n",
+ "weights2 /= 10\n",
+ "\n",
+ "weight_exp = 7\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "1802b046",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "num_steps=16\n",
+ "\n",
+ "inp_data = np.zeros((vec.shape[0], num_steps))\n",
+ "inp_data[:, 1] = vec.ravel()\n",
+ "inp_data[:, 3] = 4*vec.ravel()\n",
+ "inp_data[:, 5] = 16*vec.ravel()\n",
+ "inp_data[:, 7] = 64*vec.ravel()\n",
+ "inp_data[:, 9] = 256*vec.ravel()\n",
+ "\n",
+ "inp_data2 = np.zeros((vec2.shape[0], num_steps))\n",
+ "inp_data2[:, 1] = vec2.ravel()\n",
+ "inp_data2[:, 3] = 4*vec2.ravel()\n",
+ "inp_data2[:, 5] = 16*vec2.ravel()\n",
+ "inp_data2[:, 7] = 64*vec2.ravel()\n",
+ "inp_data2[:, 9] = 256*vec2.ravel()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "a108c232",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# instantiate the objects\n",
+ "invec1 = lv.InputVec(inp_data, loihi2=use_loihi2)\n",
+ "invec2 = lv.InputVec(inp_data2, loihi2=use_loihi2)\n",
+ "\n",
+ "in_out_syn1 = lv.GradedDense(weights=weights, exp=weight_exp)\n",
+ "in_out_syn2 = lv.GradedDense(weights=weights2, exp=weight_exp)\n",
+ "\n",
+ "outvec = lv.GradedVec(shape=(weights.shape[0],), vth=1)\n",
+ "\n",
+ "out_monitor = lv.OutputVec(shape=outvec.shape, buffer=num_steps, loihi2=use_loihi2)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "5c7e9258",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 15,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# compute the dot product of both input streams and add together\n",
+ "outvec << in_out_syn1 @ invec1 + in_out_syn2 @ invec2\n",
+ "out_monitor << outvec"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "e44b8e6f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "try:\n",
+ " outvec.run(condition=lv.RunSteps(num_steps=num_steps),\n",
+ " run_cfg=run_cfg) # Loihi2SimCfg(select_tag='fixed_pt')\n",
+ " out_data = out_monitor.get_data()\n",
+ "finally:\n",
+ " outvec.stop()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "b0c7abf8",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([[ 120, 478, 1909, 7630, 30520],\n",
+ " [ 17, 69, 271, 1080, 4320],\n",
+ " [ 22, 83, 340, 1360, 5440]], dtype=int32)"
+ ]
+ },
+ "execution_count": 17,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "out_data[:,2:11:2]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "97106421",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([120., 17., 22.])"
+ ]
+ },
+ "execution_count": 18,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "weights @ vec + weights2 @ vec2"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3ebf3be9",
+ "metadata": {},
+ "source": [
+ "Again we see the output results matching the numpy calculations, perhaps with some differences due to rounding."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4f773ae8",
+ "metadata": {},
+ "source": [
+ "## More algebra syntax\n",
+ "\n",
+ "Another function that occurs under-the-hood is the creation of Identity connections when connecting vectors. \n",
+ "\n",
+ "This can also be supported with the addition operator.\n",
+ "\n",
+ "Just have to make sure the vector shapes are correct!"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "id": "aa7f0c48",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Defining two input streams\n",
+ "vec = np.array([40, 30, 20, 10])\n",
+ "weights = np.zeros((4,4))\n",
+ "weights[:, 0] = [8, 9, -7, -2]\n",
+ "weights[:, 1] = [9, 8, -5, 2]\n",
+ "weights[:, 2] = [8, -10, -4, 5]\n",
+ "weights[:, 3] = [8, -10, -3, -9]\n",
+ "\n",
+ "vec2 = np.array([50, -50, 20, -20])\n",
+ "weights2 = np.zeros((4,4))\n",
+ "weights2[:, 0] = [3, -5, 4, -6]\n",
+ "weights2[:, 1] = [0, -2, -10, 0]\n",
+ "weights2[:, 2] = [6, 8, -4, 4]\n",
+ "weights2[:, 3] = [-5, 7, -7, 8]\n",
+ "\n",
+ "weights3 = np.random.randint(20, size=(4,4)) - 10\n",
+ "weights3 = weights3 / 10\n",
+ "\n",
+ "weights /= 10\n",
+ "weights2 /= 10\n",
+ "\n",
+ "weight_exp = 7"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "id": "7d7aaff8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "num_steps=16\n",
+ "\n",
+ "inp_data = np.zeros((vec.shape[0], num_steps))\n",
+ "inp_data[:, 1] = vec.ravel()\n",
+ "inp_data[:, 3] = 4*vec.ravel()\n",
+ "inp_data[:, 5] = 16*vec.ravel()\n",
+ "inp_data[:, 7] = 64*vec.ravel()\n",
+ "inp_data[:, 9] = 256*vec.ravel()\n",
+ "\n",
+ "inp_data2 = np.zeros((vec2.shape[0], num_steps))\n",
+ "inp_data2[:, 1] = vec2.ravel()\n",
+ "inp_data2[:, 3] = 4*vec2.ravel()\n",
+ "inp_data2[:, 5] = 16*vec2.ravel()\n",
+ "inp_data2[:, 7] = 64*vec2.ravel()\n",
+ "inp_data2[:, 9] = 256*vec2.ravel()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "id": "237a8090",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# instantiate the objects\n",
+ "invec1 = lv.InputVec(inp_data, loihi2=use_loihi2)\n",
+ "invec2 = lv.InputVec(inp_data2, loihi2=use_loihi2)\n",
+ "\n",
+ "in_out_syn1 = lv.GradedDense(weights=weights, exp=weight_exp)\n",
+ "in_out_syn2 = lv.GradedDense(weights=weights2, exp=weight_exp)\n",
+ "\n",
+ "extra_syn = lv.GradedDense(weights=weights3, exp=weight_exp)\n",
+ "\n",
+ "intvec1 = lv.GradedVec(shape=(weights.shape[0],), vth=1)\n",
+ "intvec2 = lv.GradedVec(shape=(weights.shape[0],), vth=1)\n",
+ "\n",
+ "outvec = lv.GradedVec(shape=(weights.shape[0],), vth=1)\n",
+ "out_monitor = lv.OutputVec(shape=outvec.shape, buffer=num_steps, loihi2=use_loihi2)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "id": "20055ec5",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 22,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "intvec1 << in_out_syn1 @ invec1\n",
+ "intvec2 << in_out_syn2 @ invec2\n",
+ "\n",
+ "outvec << intvec1 + intvec2 + extra_syn @ intvec1\n",
+ "out_monitor << outvec\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "id": "7d9a8b98",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "try:\n",
+ " outvec.run(condition=lv.RunSteps(num_steps=num_steps),\n",
+ " run_cfg=run_cfg) # Loihi2SimCfg(select_tag='fixed_pt')\n",
+ " out_data = out_monitor.get_data()\n",
+ "finally:\n",
+ " outvec.stop()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "id": "c45ca11b",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([[ 168, 670, 2678, 10705, 42817],\n",
+ " [ -7, -28, -118, -476, -1900],\n",
+ " [ 123, 484, 1941, 7757, 31030],\n",
+ " [ -68, -276, -1103, -4415, -17659]], dtype=int32)"
+ ]
+ },
+ "execution_count": 24,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "out_data[:,3:12:2]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "id": "c7c61f8b",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([168.8, -7.4, 123.8, -69.6])"
+ ]
+ },
+ "execution_count": 25,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "weights @ vec + weights2 @ vec2 + weights3 @ weights @ vec"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4217586c",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.10"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/tutorials/lava_va/Tutorial02-Fixed_point_elementwise_product.ipynb b/tutorials/lava_va/Tutorial02-Fixed_point_elementwise_product.ipynb
new file mode 100644
index 000000000..e438802d0
--- /dev/null
+++ b/tutorials/lava_va/Tutorial02-Fixed_point_elementwise_product.ipynb
@@ -0,0 +1,361 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "2e52884c",
+ "metadata": {},
+ "source": [
+ "*Copyright (C) 2022-23 Intel Corporation*
\n",
+ "*SPDX-License-Identifier: BSD-3-Clause*
\n",
+ "*See: https://spdx.org/licenses/*\n",
+ "\n",
+ "---\n",
+ "\n",
+ "# Tutorial 2: Elementwise products\n",
+ "\n",
+ "**Motivation:** In this tutorial, we will highlight more of the standard library included with Lava-VA. Here we demonstrate the element-wise product of vectors using ProductVec.\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "64990f4f",
+ "metadata": {},
+ "source": [
+ "First, we make the imports and connect to Loihi 2."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "72b82564",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pylab import *"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "af33e13b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import lava.frameworks.loihi2 as lv"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "2a68a562",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Running on Loihi 2\n"
+ ]
+ }
+ ],
+ "source": [
+ "from lava.utils import loihi\n",
+ "\n",
+ "loihi.use_slurm_host(loihi_gen=loihi.ChipGeneration.N3B3)\n",
+ "use_loihi2 = loihi.is_installed()\n",
+ "\n",
+ "if use_loihi2:\n",
+ " run_cfg = lv.Loihi2HwCfg()\n",
+ " print(\"Running on Loihi 2\")\n",
+ "else:\n",
+ " run_cfg = lv.Loihi2SimCfg(select_tag='fixed_pt')\n",
+ " print(\"Loihi2 compiler is not available in this system. \"\n",
+ " \"This tutorial will execute on CPU backend.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c1ff4fcd",
+ "metadata": {},
+ "source": [
+ "Next, we will setup the inputs and initialize the input weights."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "d0065b52",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "num_steps = 10\n",
+ "weights1 = np.zeros((5,1))\n",
+ "weights2 = np.zeros((5,1))\n",
+ "\n",
+ "weights1[:,0] = [2, 6, 10, -2, -6]\n",
+ "weights2[:,0] = [4, 8, 12, -4, 8]\n",
+ "\n",
+ "weights1 /= 16\n",
+ "weights2 /= 16\n",
+ "\n",
+ "inp_shape = (weights1.shape[1],)\n",
+ "out_shape = (weights1.shape[0],)\n",
+ "\n",
+ "inp_data = np.zeros((inp_shape[0], num_steps))\n",
+ "inp_data[:, 2] = 16\n",
+ "inp_data[:, 6] = 32"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5c40fe2e",
+ "metadata": {},
+ "source": [
+ "Then we instantiate the objects in the network."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "b6f6a23c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dense1 = lv.GradedDense(weights=weights1)\n",
+ "dense2 = lv.GradedDense(weights=weights2)\n",
+ "\n",
+ "vec = lv.ProductVec(shape=out_shape, vth=1, exp=0)\n",
+ "\n",
+ "generator1 = lv.InputVec(inp_data, loihi2=use_loihi2)\n",
+ "generator2 = lv.InputVec(inp_data, loihi2=use_loihi2)\n",
+ "monitor = lv.OutputVec(shape=out_shape, buffer=num_steps,\n",
+ " loihi2=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5bddeb53",
+ "metadata": {},
+ "source": [
+ "In this case, ProductVec is an object that has two input channels. We can access those input channels by concatenating the objects and \"piping\" them into the ProductVec layer. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "73392fbe",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "vec << (dense1 @ generator1, dense2 @ generator2)\n",
+ "monitor << vec"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "aaff1295",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "try:\n",
+ " vec.run(condition=lv.RunSteps(num_steps=num_steps),\n",
+ " run_cfg=run_cfg)\n",
+ " out_data = monitor.get_data()\n",
+ "finally:\n",
+ " vec.stop()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "154e2cb5",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([[ 8, 32],\n",
+ " [ 48, 192],\n",
+ " [ 120, 480],\n",
+ " [ 8, 32],\n",
+ " [ -48, -192]], dtype=int32)"
+ ]
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "out_data[:, (3,7)]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "af0e7ae4",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([ 8., 48., 120., 8., -48.])"
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "(weights1 @ inp_data[:,2]) * (weights2 @ inp_data[:,2])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "37462a5e",
+ "metadata": {},
+ "source": [
+ "We can see that this matches the numpy calculation."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8b442585",
+ "metadata": {},
+ "source": [
+ "## Multiplication operator overload\n",
+ "\n",
+ "Similar to addition, the multiplication operator is overloaded inside of GradedVec to enable the use of algebraic syntax to compute the elementwise product.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "cfaeb662",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dense1 = lv.GradedDense(weights=weights1)\n",
+ "dense2 = lv.GradedDense(weights=weights2)\n",
+ "\n",
+ "vec1 = lv.GradedVec(shape=out_shape, vth=1, exp=0)\n",
+ "vec2 = lv.GradedVec(shape=out_shape, vth=1, exp=0)\n",
+ "\n",
+ "outvec = lv.GradedVec(shape=out_shape, vth=1, exp=0)\n",
+ "\n",
+ "generator1 = lv.InputVec(inp_data, loihi2=use_loihi2)\n",
+ "generator2 = lv.InputVec(inp_data, loihi2=use_loihi2)\n",
+ "monitor = lv.OutputVec(shape=out_shape, buffer=num_steps,\n",
+ " loihi2=True)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "709a42d5",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "vec1 << dense1 @ generator1\n",
+ "vec2 << dense2 @ generator2\n",
+ "outvec << vec1 * vec2\n",
+ "monitor << outvec"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "3a0fc3a4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "try:\n",
+ " vec1.run(condition=lv.RunSteps(num_steps=num_steps),\n",
+ " run_cfg=run_cfg)\n",
+ " out_data = monitor.get_data()\n",
+ "finally:\n",
+ " vec1.stop()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "95ca186e",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([[ 8],\n",
+ " [ 48],\n",
+ " [120],\n",
+ " [ 8],\n",
+ " [-48]], dtype=int32)"
+ ]
+ },
+ "execution_count": 13,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "out_data[:, (5,)] "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6e80b63c",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.10"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/tutorials/lava_va/Tutorial03-Normalization_network.ipynb b/tutorials/lava_va/Tutorial03-Normalization_network.ipynb
new file mode 100644
index 000000000..3651ee8c7
--- /dev/null
+++ b/tutorials/lava_va/Tutorial03-Normalization_network.ipynb
@@ -0,0 +1,319 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "37ee11cd",
+ "metadata": {},
+ "source": [
+ "*Copyright (C) 2022-23 Intel Corporation*
\n",
+ "*SPDX-License-Identifier: BSD-3-Clause*
\n",
+ "*See: https://spdx.org/licenses/*\n",
+ "\n",
+ "---\n",
+ "\n",
+ "# Tutorial 3: Normalization Network\n",
+ "\n",
+ "**Motivation:** In this tutorial, we will highlight more of the standard library included with Lava-VA. Here we demonstrate the Normalization network for computing a normalized vector output from a dot product.\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "5822757f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pylab import *"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2dc9a58e",
+ "metadata": {},
+ "source": [
+ "Here again we setup the imports and connect to Loihi 2."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "086079fb",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import lava.frameworks.loihi2 as lv"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "67b4603d",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Running on Loihi 2\n"
+ ]
+ }
+ ],
+ "source": [
+ "from lava.utils import loihi\n",
+ "\n",
+ "loihi.use_slurm_host(loihi_gen=loihi.ChipGeneration.N3B3)\n",
+ "use_loihi2 = loihi.is_installed()\n",
+ "\n",
+ "if use_loihi2:\n",
+ " run_cfg = lv.Loihi2HwCfg()\n",
+ " print(\"Running on Loihi 2\")\n",
+ "else:\n",
+ " run_cfg = lv.Loihi2SimCfg(select_tag='fixed_pt')\n",
+ " print(\"Loihi2 compiler is not available in this system. \"\n",
+ " \"This tutorial will execute on CPU backend.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fb525826",
+ "metadata": {},
+ "source": [
+ "Now we will create some inputs and initialize the weight matrix."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "82d4446c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "num_steps=20\n",
+ "weights = np.zeros((10,1))\n",
+ "\n",
+ "weights[:,0] = [2, 4, -2, -4, 8, 10, -8, -10, 5, -5]\n",
+ "weights /= 10\n",
+ "\n",
+ "inp_data = np.zeros((weights.shape[1], num_steps))\n",
+ "\n",
+ "inp_data[:, 2] = 160\n",
+ "inp_data[:, 5] = 320\n",
+ "inp_data[:, 8] = 640\n",
+ "inp_data[:, 11] = 1280\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f77b1746",
+ "metadata": {},
+ "source": [
+ "In this demo, we set the input stimulus to be of different magnitudes. The inputs will multiply with the weight values, producing a vector output. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "151073fb",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([[ 0.2],\n",
+ " [ 0.4],\n",
+ " [-0.2],\n",
+ " [-0.4],\n",
+ " [ 0.8],\n",
+ " [ 1. ],\n",
+ " [-0.8],\n",
+ " [-1. ],\n",
+ " [ 0.5],\n",
+ " [-0.5]])"
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "weights"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "678f6873",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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IFZUVMBut8NgMLg8bzAYYDabO3yN2Wz2qL0vOBwOLFiwALfddhuuv/56t+2SkpKwYcMGfPTRR3jrrbfgcDhw66234qeffvJZX1NSUrBp0ybs3LkT69atw7lz53DHHXdItSvbU+L1BoBt27ahuroajz76qNs2Srje7YnXzZNr2pOfE29rbGzEU089hYceeqjTiguefr95y+TJk/Hmm28iNzcXL730Evbu3YspU6bAbrd32F6J13zz5s2IiIjA/fff32k7pVzznrpqyz9dKzIzM3H8+PEu5+lTU1ORmpoqfX3rrbdi5MiReO211/D88897u5sAgClTpkj/Hjt2LFJSUjBo0CC899573frLUyneeOMNTJkyBSaTyW0bJVzvq5HNZsMDDzwAQRCwbt26Ttsq5ftt9uzZ0r/HjBmDsWPHYujQodizZw8mTpzos370xoYNG5CRkdFlYphSrnlPXbMjxNjYWGg0GpSVlbk8X1ZWBqPR2OFrjEajR+29LSsrCzt27MDu3bs9rv0YHByMG264Ad99952Xete1qKgoDB8+3G0flHa9AeD8+fP44osv8B//8R8evU4J11u8bp5c0578nHiLGAzPnz+PnJwcj+vxdfX95itDhgxBbGys234o6ZoDwJdffomioiKPv+cB5Vzz7rpmA6JWq8X48eORm5srPedwOJCbm+vyl31bqampLu0BICcnx217bxEEAVlZWfjwww+xa9cuJCYmenwOu92OY8eOIT4+3gs97J66ujp8//33bvuglOvd1saNGxEXF4dp06Z59DolXO/ExEQYjUaXa2qxWJCfn+/2mvbk58QbxGB49uxZfPHFF4iJifH4HF19v/nKTz/9hMrKSrf9UMo1F73xxhsYP348kpOTPX6tUq55t/k7q8ef3n33XSEkJETYtGmTcPLkSWH+/PlCVFSUYDabBUEQhIcfflh4+umnpfZff/21EBQUJKxYsUI4deqUsHTpUiE4OFg4duyYT/v92GOPCXq9XtizZ49w4cIF6dHQ0CC1ad/35557Tvjss8+E77//XigoKBBmz54t6HQ64cSJEz7r95///Gdhz549wrlz54Svv/5aSEtLE2JjY4Xy8vIO+6yU6y2y2+3CwIEDhaeeeuqKY0q53rW1tcKRI0eEI0eOCACElStXCkeOHJGyMV988UUhKipK+Oijj4Rvv/1WmDFjhpCYmChcvnxZOsc999wjrFmzRvq6q58Tb/fbarUK9913nzBgwAChsLDQ5Xu+qanJbb+7+n7zRd9ra2uFv/zlL0JeXp5w7tw54YsvvhBuvPFGYdiwYUJjY6Pbvvv7motqamqEsLAwYd26dR2ew1/X3Fuu6YAoCIKwZs0aYeDAgYJWqxVuvvlm4cCBA9KxX/ziF8KcOXNc2r/33nvC8OHDBa1WK4wePVr45JNPfNxjZ4p0R4+NGzdKbdr3fcGCBdLnNBgMwtSpU4XDhw/7tN8PPvigEB8fL2i1WqF///7Cgw8+KHz33Xdu+ywIyrjeos8++0wAIBQVFV1xTCnXe/fu3R1+b4h9czgcwuLFiwWDwSCEhIQIEydOvOLzDBo0SFi6dKnLc539nHi73+fOnXP7Pb979263/e7q+80XfW9oaBAmTZok9OvXTwgODhYGDRokzJs374rAprRrLnrttdeE0NBQobq6usNz+OuaewvrIRIREeEavodIRETUFgMiERERGBCJiIgAMCASEREBYEAkIiICwIBIREQEgAGRiIgIAAMiERERAAZEIiIiAAyIREREABgQiYiIAAD/H64bkHOSCymtAAAAAElFTkSuQmCC",
+ "text/plain": [
+ "