From 0f2dd9b54e0bfac86967f096c6bf90f8eb8e13bb Mon Sep 17 00:00:00 2001 From: smm-ncl Date: Thu, 11 Jan 2024 05:16:26 -0800 Subject: [PATCH 1/7] first wokring version --- src/lava/proc/s4d/models.py | 96 ++++++++++++++++++++++++++++++++++++ src/lava/proc/s4d/process.py | 45 +++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 src/lava/proc/s4d/models.py create mode 100644 src/lava/proc/s4d/process.py diff --git a/src/lava/proc/s4d/models.py b/src/lava/proc/s4d/models.py new file mode 100644 index 000000000..07ef8e261 --- /dev/null +++ b/src/lava/proc/s4d/models.py @@ -0,0 +1,96 @@ +from lava.proc.sdn.models import AbstractSigmaModel, AbstractDeltaModel +import typing as ty +from typing import Any, Dict +from lava.magma.core.decorator import implements, requires, tag +import numpy as np +from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol +from lava.proc.s4d.process import SigmaS4Delta +from lava.magma.core.resources import CPU +from lava.magma.core.model.py.ports import PyInPort, PyOutPort +from lava.magma.core.model.py.type import LavaPyType + +class AbstractSigmaS4DeltaModel(AbstractSigmaModel, AbstractDeltaModel): + a_in = None + s_out = None + + vth = None + sigma = None + act = None + residue = None + error = None + state_exp = None + + A = None + B = None + C = None + S4state = None + + + def __init__(self, proc_params: Dict[str, Any]) -> None: + super().__init__(proc_params) + self.A = self.proc_params['A'] + self.B = self.proc_params['B'] + self.C = self.proc_params['C'] + self.S4state = self.proc_params['S4state'] + + def activation_dynamics(self, sigma_data: np.ndarray) -> np.ndarray: + """Sigma Delta activation dynamics. UNIT and RELU activations are + supported. + + Parameters + ---------- + sigma_data : np.ndarray + sigma decoded data + + Returns + ------- + np.ndarray + activation output + + + Equations: + initial state = model.default_state (apparently all zeros) + new_state = state * A + inp * B + out = c * new_state * 2 # mal zwei remains unclear + """ + self.S4state = self.S4state * self.A + sigma_data * self.B + act = self.C * self.S4state * 2 + return act + + def dynamics(self, a_in_data: np.ndarray) -> np.ndarray: + self.sigma = self.sigma_dynamics(a_in_data) + act = self.activation_dynamics(self.sigma) + s_out = self.delta_dynamics(act) + self.act = act + return s_out + + +@implements(proc=SigmaS4Delta, protocol=LoihiProtocol) +@requires(CPU) +@tag('floating_pt') +class PySigmaS4DeltaModelFloat(AbstractSigmaS4DeltaModel): + """Floating point implementation of Sigma Delta neuron.""" + a_in = LavaPyType(PyInPort.VEC_DENSE, float) + s_out = LavaPyType(PyOutPort.VEC_DENSE, float) + + vth: np.ndarray = LavaPyType(np.ndarray, float) + sigma: np.ndarray = LavaPyType(np.ndarray, float) + act: np.ndarray = LavaPyType(np.ndarray, float) + residue: np.ndarray = LavaPyType(np.ndarray, float) + error: np.ndarray = LavaPyType(np.ndarray, float) + + state_exp: np.ndarray = LavaPyType(np.ndarray, np.int32, precision=3) + + + # S4 stuff + S4state: np.ndarray = LavaPyType(np.ndarray, float) + A: np.ndarray = LavaPyType(np.ndarray, float) + B: np.ndarray = LavaPyType(np.ndarray, float) + C: np.ndarray = LavaPyType(np.ndarray, float) + + + def run_spk(self) -> None: + # Receive synaptic input + a_in_data = self.a_in.recv() + s_out = self.dynamics(a_in_data) + self.s_out.send(s_out) \ No newline at end of file diff --git a/src/lava/proc/s4d/process.py b/src/lava/proc/s4d/process.py new file mode 100644 index 000000000..107baa1ac --- /dev/null +++ b/src/lava/proc/s4d/process.py @@ -0,0 +1,45 @@ +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 +import typing as ty + + + +class SigmaS4Delta(AbstractProcess): + def __init__( + self, + *, + shape: ty.Tuple[int, ...], + vth: float, + A: float, + B: float, + C: float, + state_exp: ty.Optional[int] = 0) -> None: + """Sigma delta neuron process. That has S4d as its activation function""" + + self.S4state = Var(shape=shape, init=0) + super().__init__(shape=shape, vth=vth, A=A, B=B, C=C, S4state=self.S4state, state_exp=state_exp) + # scaling factor for fixed precision scaling + vth = vth * (1 << (state_exp)) + + self.a_in = InPort(shape=shape) + self.s_out = OutPort(shape=shape) + + # Variables for SigmaDelta + self.vth = Var(shape=(1,), init=vth) + self.sigma = Var(shape=shape, init=0) + self.act = Var(shape=shape, init=0) + self.residue = Var(shape=shape, init=0) + self.error = Var(shape=shape, init=0) + self.state_exp = Var(shape=(1,), init=state_exp) + + # Variables for S4 + self.A = Var(shape=shape, init=A) + self.B = Var(shape=shape, init=B) + self.C = Var(shape=shape, init=C) + + + @property + def shape(self) -> ty.Tuple[int, ...]: + """Return shape of the Process.""" + return self.proc_params['shape'] \ No newline at end of file From f44df1df5f9f308151887a6776a1751210035f01 Mon Sep 17 00:00:00 2001 From: smm-ncl Date: Thu, 18 Jan 2024 02:35:57 -0800 Subject: [PATCH 2/7] S4D model cleaned --- src/lava/proc/s4d/models.py | 113 ++++++++++++---- src/lava/proc/s4d/process.py | 193 ++++++++++++++++++++++++---- src/lava/proc/sdn/process.py | 5 +- tests/lava/proc/s4d/s4d_A.dat.npy | 3 + tests/lava/proc/s4d/s4d_B.dat.npy | 3 + tests/lava/proc/s4d/s4d_C.dat.npy | 3 + tests/lava/proc/s4d/test_models.py | 179 ++++++++++++++++++++++++++ tests/lava/proc/s4d/test_process.py | 83 ++++++++++++ tests/lava/proc/s4d/utils.py | 85 ++++++++++++ 9 files changed, 610 insertions(+), 57 deletions(-) create mode 100644 tests/lava/proc/s4d/s4d_A.dat.npy create mode 100644 tests/lava/proc/s4d/s4d_B.dat.npy create mode 100644 tests/lava/proc/s4d/s4d_C.dat.npy create mode 100644 tests/lava/proc/s4d/test_models.py create mode 100644 tests/lava/proc/s4d/test_process.py create mode 100644 tests/lava/proc/s4d/utils.py diff --git a/src/lava/proc/s4d/models.py b/src/lava/proc/s4d/models.py index 07ef8e261..bb0ed3086 100644 --- a/src/lava/proc/s4d/models.py +++ b/src/lava/proc/s4d/models.py @@ -1,30 +1,39 @@ -from lava.proc.sdn.models import AbstractSigmaModel, AbstractDeltaModel -import typing as ty +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# See: https://spdx.org/licenses/ + +from lava.proc.sdn.models import AbstractSigmaDeltaModel from typing import Any, Dict from lava.magma.core.decorator import implements, requires, tag import numpy as np from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol -from lava.proc.s4d.process import SigmaS4Delta +from lava.proc.s4d.process import SigmaS4Delta, SigmaS4DeltaLayer from lava.magma.core.resources import CPU from lava.magma.core.model.py.ports import PyInPort, PyOutPort from lava.magma.core.model.py.type import LavaPyType +from lava.magma.core.model.sub.model import AbstractSubProcessModel +from lava.proc.sparse.process import Sparse + -class AbstractSigmaS4DeltaModel(AbstractSigmaModel, AbstractDeltaModel): +class AbstractSigmaS4DeltaModel(AbstractSigmaDeltaModel): a_in = None s_out = None + # SigmaDelta Variables vth = None sigma = None act = None residue = None error = None state_exp = None - + bias = None + + # S4 Variables A = None B = None C = None S4state = None - + S4_exp = None def __init__(self, proc_params: Dict[str, Any]) -> None: super().__init__(proc_params) @@ -34,42 +43,47 @@ def __init__(self, proc_params: Dict[str, Any]) -> None: self.S4state = self.proc_params['S4state'] def activation_dynamics(self, sigma_data: np.ndarray) -> np.ndarray: - """Sigma Delta activation dynamics. UNIT and RELU activations are - supported. + """Sigma Delta activation dynamics. Performs S4D dynamics. Parameters ---------- - sigma_data : np.ndarray + sigma_data: np.ndarray sigma decoded data Returns ------- np.ndarray activation output - - - Equations: - initial state = model.default_state (apparently all zeros) - new_state = state * A + inp * B - out = c * new_state * 2 # mal zwei remains unclear + + Notes + ----- + This function simulates the behavior of a linear time-invariant system + with diagonalized state-space representation. (S4D) + The state-space equations are given by: + x_{k+1} = A * x_k + B * u_k + y_k = C * x_k + + where: + - x_k is the state vector at time step k, + - u_k is the input vector at time step k, + - y_k is the output vector at time step k, + - A is the diagonal state matrix, + - B is the diagonal input matrix, + - C is the diagonal output matrix. + + The function computes the next output step of the system for the given input signal. """ + self.S4state = self.S4state * self.A + sigma_data * self.B act = self.C * self.S4state * 2 return act - def dynamics(self, a_in_data: np.ndarray) -> np.ndarray: - self.sigma = self.sigma_dynamics(a_in_data) - act = self.activation_dynamics(self.sigma) - s_out = self.delta_dynamics(act) - self.act = act - return s_out - @implements(proc=SigmaS4Delta, protocol=LoihiProtocol) @requires(CPU) @tag('floating_pt') class PySigmaS4DeltaModelFloat(AbstractSigmaS4DeltaModel): - """Floating point implementation of Sigma Delta neuron.""" + """Floating point implementation of SigmaS4Delta neuron.""" a_in = LavaPyType(PyInPort.VEC_DENSE, float) s_out = LavaPyType(PyOutPort.VEC_DENSE, float) @@ -78,19 +92,62 @@ class PySigmaS4DeltaModelFloat(AbstractSigmaS4DeltaModel): act: np.ndarray = LavaPyType(np.ndarray, float) residue: np.ndarray = LavaPyType(np.ndarray, float) error: np.ndarray = LavaPyType(np.ndarray, float) - + bias: np.ndarray = LavaPyType(np.ndarray, float) + state_exp: np.ndarray = LavaPyType(np.ndarray, np.int32, precision=3) - - + cum_error: np.ndarray = LavaPyType(np.ndarray, bool, precision=1) + spike_exp: np.ndarray = LavaPyType(np.ndarray, np.int32, precision=3) + S4_exp: np.ndarray = LavaPyType(np.ndarray, np.int32, precision=3) + # S4 stuff S4state: np.ndarray = LavaPyType(np.ndarray, float) A: np.ndarray = LavaPyType(np.ndarray, float) B: np.ndarray = LavaPyType(np.ndarray, float) C: np.ndarray = LavaPyType(np.ndarray, float) - def run_spk(self) -> None: # Receive synaptic input a_in_data = self.a_in.recv() s_out = self.dynamics(a_in_data) - self.s_out.send(s_out) \ No newline at end of file + self.s_out.send(s_out) + + +@implements(proc=SigmaS4DeltaLayer, protocol=LoihiProtocol) +class SubDenseLayerModel(AbstractSubProcessModel): + def __init__(self, proc): + """Builds (Sparse -> S4D -> Sparse) connection of the process.""" + conn_weights = proc.proc_params.get("conn_weights") + shape = proc.proc_params.get("shape") + state_exp = proc.proc_params.get("state_exp") + num_message_bits = proc.proc_params.get("num_message_bits") + S4_exp = proc.proc_params.get("S4_exp") + d_states = proc.proc_params.get("d_states") + A = proc.proc_params.get("A") + B = proc.proc_params.get("B") + C = proc.proc_params.get("C") + vth = proc.proc_params.get("vth") + + # Instantiate processes + self.sparse1 = Sparse(weights=conn_weights.T, weight_exp=state_exp, + num_message_bits=num_message_bits) + self.sigmaS4delta = SigmaS4Delta(shape=(shape[0] * d_states,), + vth=vth, + state_exp=state_exp, + S4_exp=S4_exp, + A=A, + B=B, + C=C) + self.sparse2 = Sparse(weights=conn_weights, weight_exp=state_exp, + num_message_bits=num_message_bits) + + # Make connections Sparse -> SigmaS4Delta -> Sparse + proc.in_ports.s_in.connect(self.sparse1.in_ports.s_in) + self.sparse1.out_ports.a_out.connect(self.sigmaS4delta.in_ports.a_in) + self.sigmaS4delta.out_ports.s_out.connect(self.sparse2.s_in) + self.sparse2.out_ports.a_out.connect(proc.out_ports.a_out) + + # Set aliasses + proc.vars.A.alias(self.sigmaS4delta.vars.A) + proc.vars.B.alias(self.sigmaS4delta.vars.B) + proc.vars.C.alias(self.sigmaS4delta.vars.C) + proc.vars.S4state.alias(self.sigmaS4delta.vars.S4state) diff --git a/src/lava/proc/s4d/process.py b/src/lava/proc/s4d/process.py index 107baa1ac..e400f31ec 100644 --- a/src/lava/proc/s4d/process.py +++ b/src/lava/proc/s4d/process.py @@ -1,45 +1,184 @@ -from lava.magma.core.process.process import AbstractProcess +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# See: https://spdx.org/licenses/ + +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 +from lava.proc.sdn.process import ActivationMode, SigmaDelta import typing as ty +import numpy as np - -class SigmaS4Delta(AbstractProcess): +class SigmaS4Delta(SigmaDelta, AbstractProcess): def __init__( self, - *, shape: ty.Tuple[int, ...], - vth: float, + vth: ty.Union[int, float], A: float, B: float, C: float, - state_exp: ty.Optional[int] = 0) -> None: - """Sigma delta neuron process. That has S4d as its activation function""" - - self.S4state = Var(shape=shape, init=0) - super().__init__(shape=shape, vth=vth, A=A, B=B, C=C, S4state=self.S4state, state_exp=state_exp) - # scaling factor for fixed precision scaling - vth = vth * (1 << (state_exp)) - - self.a_in = InPort(shape=shape) - self.s_out = OutPort(shape=shape) - - # Variables for SigmaDelta - self.vth = Var(shape=(1,), init=vth) - self.sigma = Var(shape=shape, init=0) - self.act = Var(shape=shape, init=0) - self.residue = Var(shape=shape, init=0) - self.error = Var(shape=shape, init=0) - self.state_exp = Var(shape=(1,), init=state_exp) - - # Variables for S4 + state_exp: ty.Optional[int] = 0, + S4_exp: ty.Optional[int] = 0) -> None: + """ + Sigma delta neuron process that implements S4D dynamics as its + activation function. + + Parameters + ---------- + shape: Tuple + Shape of the sigma process. + vth: int or float + Threshold of the delta encoder. + A: np.ndarray + Diagonal elements of the state matrix of the S4D model. + B: np.ndarray + Diagonal elements of the input matrix of the S4D model. + C: np.ndarray + Diagonal elements of the output matrix of the S4D model. + state_exp: int + Scaling exponent with base 2 for the reconstructed sigma variables. + Note: This should only be used for nc models. + Default is 0. + S4_exp: int + Scaling exponent with base 2 for the S4 state variables. + Note: This should only be used for nc models. + Default is 0. + + Notes + ----- + This process simulates the behavior of a linear time-invariant system with state-space representation. + The state-space equations are given by: + x_{k+1} = A * x_k + B * u_k + y_k = C * x_k + + where: + - x_k is the state vector at time step k, + - u_k is the input vector at time step k, + - y_k is the output vector at time step k, + - A is the diagonal state matrix, + - B is the diagonal input matrix, + - C is the diagonal output matrix. + """ + + super().__init__(shape=shape, + vth=vth, + A=A, + B=B, + C=C, + S4state=0, + state_exp=state_exp, + S4_exp=S4_exp) + + # Variables for S4 self.A = Var(shape=shape, init=A) self.B = Var(shape=shape, init=B) self.C = Var(shape=shape, init=C) - + self.S4state = Var(shape=shape, init=0) + self.S4_exp = Var(shape=(1,), init=S4_exp) + + +class SigmaS4DeltaLayer(AbstractProcess): + def __init__( + self, + shape: ty.Tuple[int, ...], + vth: ty.Union[int, float], + A: float, + B: float, + C: float, + d_states: ty.Optional[int] = 1, + S4_exp: ty.Optional[int] = 0, + num_message_bits: ty.Optional[int] = 24, + state_exp: ty.Optional[int] = 0) -> None: + """ + Combines S4D neuron with Sparse Processes that allow for multiple + d_states. + + Parameters + ---------- + shape: Tuple + Shape of the sigma process. + vth: int or float + Threshold of the delta encoder. + A: np.ndarray + Diagonal elements of the state matrix of the S4D model. + B: np.ndarray + Diagonal elements of the input matrix of the S4D model. + C: np.ndarray + Diagonal elements of the output matrix of the S4D model. + d_states: int + Number of hidden states of the S4D model. + Default is 1. + state_exp: int + Scaling exponent with base 2 for the reconstructed sigma variables. + Note: Only relevant for nc model. + Default is 0. + num_message_bits: int + Number of message bits to be used in Sparse connection processes. + Note: Only relevant for nc model. + S4_exp: int + Scaling exponent with base 2 for the S4 state variables. + Note: Only relevant for nc model. + Default is 0. + + Notes + ----- + Connectivity: Sparse -> SigmaS4Delta -> Sparse. + Relieves user from computing required connection weights for multiple + d_states. + + This process simulates the behavior of a linear time-invariant system with + diagonalized state-space representation. (S4D) + The state-space equations are given by: + x_{k+1} = A * x_k + B * u_k + y_k = C * x_k + + where: + - x_k is the state vector at time step k, + - u_k is the input vector at time step k, + - y_k is the output vector at time step k, + - A is the diagonal state matrix, + - B is the diagonal input matrix, + - C is the diagonal output matrix. + """ + + # Automatically takes care of expansion and reduction of dimensionality + # for multiple hidden states (d_states) + conn_weights = np.kron(np.eye(shape[0]), np.ones(d_states)) + S4state = 0 + super().__init__(shape=shape, + vth=vth, + A=A, + B=B, + C=C, + S4_exp=S4_exp, + S4state=S4state, + conn_weights=conn_weights, + num_message_bits=num_message_bits, + d_states=d_states, + state_exp=state_exp, + act_mode=ActivationMode.UNIT) + + # Ports + self.s_in = InPort(shape=shape) + self.a_out = OutPort(shape=shape) + + # General variables + self.state_exp = Var(shape=(1,), init=state_exp) + + # Variables for S4 + self.A = Var(shape=(shape[0] * d_states,), init=A) + self.B = Var(shape=(shape[0] * d_states,), init=B) + self.C = Var(shape=(shape[0] * d_states,), init=C) + self.S4state = Var(shape=(shape[0] * d_states,), init=0) + self.S4_exp = Var(shape=(1,), init=S4_exp) + + # Variables for connecting Dense processes + # Project input_dim to input_dim * d_states + self.conn_weights = Var(shape=shape, init=conn_weights) + self.num_message_bits = Var(shape=(1,), init=num_message_bits) @property def shape(self) -> ty.Tuple[int, ...]: """Return shape of the Process.""" - return self.proc_params['shape'] \ No newline at end of file + return self.proc_params['shape'] diff --git a/src/lava/proc/sdn/process.py b/src/lava/proc/sdn/process.py index 32f083247..6de494706 100644 --- a/src/lava/proc/sdn/process.py +++ b/src/lava/proc/sdn/process.py @@ -126,7 +126,8 @@ def __init__( act_mode: ty.Optional[ActivationMode] = ActivationMode.RELU, cum_error: ty.Optional[bool] = False, spike_exp: ty.Optional[int] = 0, - state_exp: ty.Optional[int] = 0) -> None: + state_exp: ty.Optional[int] = 0, + **kwargs) -> None: """Sigma delta neuron process. At the moment only ReLu activation is supported. Spike mechanism based on accumulated error is also supported. @@ -173,7 +174,7 @@ def __init__( """ super().__init__(shape=shape, vth=vth, bias=bias, act_mode=act_mode, cum_error=cum_error, - spike_exp=spike_exp, state_exp=state_exp) + spike_exp=spike_exp, state_exp=state_exp, **kwargs) # scaling factor for fixed precision scaling vth = vth * (1 << (spike_exp + state_exp)) bias = bias * (1 << (spike_exp + state_exp)) diff --git a/tests/lava/proc/s4d/s4d_A.dat.npy b/tests/lava/proc/s4d/s4d_A.dat.npy new file mode 100644 index 000000000..081fdd6ec --- /dev/null +++ b/tests/lava/proc/s4d/s4d_A.dat.npy @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e4c9f5f11d3139b86ccdfe3d8e2179566dc8ac8da31a4a23951dba174425663 +size 5248 diff --git a/tests/lava/proc/s4d/s4d_B.dat.npy b/tests/lava/proc/s4d/s4d_B.dat.npy new file mode 100644 index 000000000..916b75ffb --- /dev/null +++ b/tests/lava/proc/s4d/s4d_B.dat.npy @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b67f99c0a172c862abfc65b3aabeba9bc91fe1f2254d0df066d19c9b3e3b8fe +size 5248 diff --git a/tests/lava/proc/s4d/s4d_C.dat.npy b/tests/lava/proc/s4d/s4d_C.dat.npy new file mode 100644 index 000000000..910f44a96 --- /dev/null +++ b/tests/lava/proc/s4d/s4d_C.dat.npy @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:055720b65a2eb0bf0043989b1a078cc028f7a105a0cb394ba03cdbf3adac8ac1 +size 5248 diff --git a/tests/lava/proc/s4d/test_models.py b/tests/lava/proc/s4d/test_models.py new file mode 100644 index 000000000..692f90df7 --- /dev/null +++ b/tests/lava/proc/s4d/test_models.py @@ -0,0 +1,179 @@ +# expressly stated in the License. +# See: https://spdx.org/licenses/ + +import unittest +import numpy as np +from lava.magma.core.run_conditions import RunSteps +from lava.proc.sdn.process import ActivationMode, SigmaDelta +from lava.proc.s4d.process import SigmaS4Delta, SigmaS4DeltaLayer +from lava.proc.sparse.process import Sparse +import lava.proc.io as io +from typing import Tuple +from lava.magma.core.run_configs import Loihi2SimCfg +from lava.tests.lava.proc.s4d.utils import get_coefficients, run_original_model + + +class TestSigmaS4DDeltaModels(unittest.TestCase): + """Tests for SigmaS4Delta neuron""" + def run_in_lava( + self, + input, + num_steps: int, + model_dim: int, + d_states: int, + use_layer: bool) -> Tuple[np.ndarray]: + + """ Run S4d model in lava. + + Parameters + ---------- + input : np.ndarray + Input signal to the model. + num_steps : int + Number of time steps to simulate the model. + model_dim : int + Dimensionality of the model. + d_states : int + Number of model states. + use_layer : bool + Whether to use the layer implementation of the model + (SigmaS4DeltaLayer, helpful for multiple d_states) or just + the neuron model (SigmaS4Delta). + + Returns + ------- + Tuple[np.ndarray] + Tuple containing the output of the model simulation. + """ + + A = self.A[:model_dim * d_states] + B = self.B[:model_dim * d_states] + C = self.C[:model_dim * d_states] + + diff = input[:, 1:] - input[:, :-1] + diff = np.concatenate((input[:, :1], diff), axis=1) + + spiker = io.source.RingBuffer(data=diff) + receiver = io.sink.RingBuffer(shape=(model_dim,), buffer=num_steps) + + if use_layer: + s4d_layer = SigmaS4DeltaLayer(shape=(model_dim,), + d_states=d_states, + num_message_bits=24, + vth=0, + A=A, + B=B, + C=C) + buffer_neuron = SigmaDelta(shape=(model_dim,), + vth=0, + cum_error=True, + act_mode=ActivationMode.UNIT) + spiker.s_out.connect(s4d_layer.s_in) + s4d_layer.a_out.connect(buffer_neuron.a_in) + buffer_neuron.s_out.connect(receiver.a_in) + + else: + sparse = Sparse(weights=np.eye(model_dim), num_message_bits=24) + s4d_neuron = SigmaS4Delta(shape=((model_dim,)), + vth=0, + A=A, + B=B, + C=C) + spiker.s_out.connect(sparse.s_in) + sparse.a_out.connect(s4d_neuron.a_in) + s4d_neuron.s_out.connect(receiver.a_in) + + run_condition = RunSteps(num_steps=num_steps) + run_config = Loihi2SimCfg() + + spiker.run(condition=run_condition, run_cfg=run_config) + output = receiver.data.get() + spiker.stop() + + output = np.cumsum(output, axis=1) + + return output + + def test_py_model_vs_original_equations(self) -> None: + """Tests that the pymodel for SigmaS4Delta outputs approximately + the same values as the original S4D equations. + """ + self.A, self.B, self.C = get_coefficients() + model_dim = 3 + d_states = 1 + n_steps = 5 + np.random.seed(0) + inp = np.random.random((model_dim, n_steps)) * 2**6 + + out_chip = self.run_in_lava(input=inp, + num_steps=n_steps, + model_dim=model_dim, + d_states=d_states, + use_layer=False + ) + out_original_model = run_original_model(input=inp, + model_dim=model_dim, + d_states=d_states, + num_steps=n_steps, + A=self.A, + B=self.B, + C=self.C) + + np.testing.assert_array_equal(out_original_model[:, :-1], + out_chip[:, 1:]) + + def test_py_model_layer_vs_original_equations(self) -> None: + """ Tests that the pymodel for SigmaS4DeltaLayer outputs approximately + the same values as the original S4D equations for multiple d_states. + """ + self.A, self.B, self.C = get_coefficients() + model_dim = 3 + d_states = 3 + n_steps = 5 + np.random.seed(1) + inp = np.random.random((model_dim, n_steps)) * 2**6 + + out_chip = self.run_in_lava(input=inp, + num_steps=n_steps, + model_dim=model_dim, + d_states=d_states, + use_layer=True, + ) + out_original_model = run_original_model(input=inp, + model_dim=model_dim, + d_states=d_states, + num_steps=n_steps, + A=self.A, + B=self.B, + C=self.C) + + np.testing.assert_allclose(out_original_model[:, :-2], out_chip[:, 2:]) + + def test_py_model_vs_py_model_layer(self) -> None: + """Tests that the pymodel for SigmaS4DeltaLayer outputs approximately + the same values as just the SigmaS4DDelta Model with one hidden dim. + """ + self.A, self.B, self.C = get_coefficients() + model_dim = 3 + d_states = 1 + n_steps = 5 + np.random.seed(2) + inp = np.random.random((model_dim, n_steps)) * 2**6 + + out_just_model = self.run_in_lava(input=inp, + num_steps=n_steps, + model_dim=model_dim, + d_states=d_states, + use_layer=False) + + out_layer = self.run_in_lava(input=inp, + num_steps=n_steps, + model_dim=model_dim, + d_states=d_states, + use_layer=True) + + np.testing.assert_allclose(out_layer[:, 1:], out_just_model[:, :-1]) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/lava/proc/s4d/test_process.py b/tests/lava/proc/s4d/test_process.py new file mode 100644 index 000000000..4eddfb92d --- /dev/null +++ b/tests/lava/proc/s4d/test_process.py @@ -0,0 +1,83 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# See: https://spdx.org/licenses/ + +import unittest +from lava.proc.s4d.process import SigmaS4Delta, SigmaS4DeltaLayer +import numpy as np + + +class TestSigmaS4DeltaProcess(unittest.TestCase): + """Tests for SigmaS4Delta Class""" + + def test_init(self) -> None: + """Tests instantiation of SigmaS4Delta""" + shape = 10 + vth = 10 + state_exp = 6 + s4_exp = 12 + A = np.ones(shape) * 0.5 + B = np.ones(shape) * 0.8 + C = np.ones(shape) * 0.9 + sigmas4delta = SigmaS4Delta(shape=(shape,), + vth=vth, + state_exp=state_exp, + S4_exp=s4_exp, + A=A, + B=B, + C=C) + + # determined by user - S4 part + self.assertEqual(sigmas4delta.shape, (shape,)) + self.assertEqual(sigmas4delta.vth.init, vth * 2 ** state_exp) + self.assertEqual(sigmas4delta.S4_exp.init, s4_exp) + np.testing.assert_array_equal(sigmas4delta.A.init, A) + np.testing.assert_array_equal(sigmas4delta.B.init, B) + np.testing.assert_array_equal(sigmas4delta.C.init, C) + self.assertEqual(sigmas4delta.state_exp.init, state_exp) + self.assertEqual(sigmas4delta.S4state.init, 0) + + # default sigmadelta params - inherited from SigmaDelta class + self.assertEqual(sigmas4delta.cum_error.init, False) + self.assertEqual(sigmas4delta.spike_exp.init, 0) + self.assertEqual(sigmas4delta.bias.init, 0) + + +class TestSigmaS4DeltaLayer(unittest.TestCase): + """Tests for SigmaS4DeltaLayer Class""" + + def test_init(self) -> None: + """Tests instantiation of SigmaS4DeltaLayer """ + shape = 10 + vth = 10 + state_exp = 6 + s4_exp = 12 + d_states = 5 + A = np.ones(shape * d_states) * 0.5 + B = np.ones(shape * d_states) * 0.8 + C = np.ones(shape * d_states) * 0.9 + + sigmas4deltalayer = SigmaS4DeltaLayer(shape=(shape,), + d_states=d_states, + vth=vth, + state_exp=state_exp, + S4_exp=s4_exp, + A=A, + B=B, + C=C) + # determined by user - S4 part + self.assertEqual(sigmas4deltalayer.shape, (shape,)) + self.assertEqual(sigmas4deltalayer.S4_exp.init, s4_exp) + np.testing.assert_array_equal(sigmas4deltalayer.A.init, A) + np.testing.assert_array_equal(sigmas4deltalayer.B.init, B) + np.testing.assert_array_equal(sigmas4deltalayer.C.init, C) + self.assertEqual(sigmas4deltalayer.state_exp.init, state_exp) + self.assertEqual(sigmas4deltalayer.S4state.init, 0) + + # determined by user/via number of states and shape + np.testing.assert_array_equal(sigmas4deltalayer.conn_weights.init, + np.kron(np.eye(shape), np.ones(d_states))) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/lava/proc/s4d/utils.py b/tests/lava/proc/s4d/utils.py new file mode 100644 index 000000000..f649c8bd1 --- /dev/null +++ b/tests/lava/proc/s4d/utils.py @@ -0,0 +1,85 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# See: https://spdx.org/licenses/ + +import os +import numpy as np +from typing import Tuple + + +def get_coefficients() -> [np.ndarray, np.ndarray, np.ndarray]: + curr_dir = os.path.dirname(os.path.realpath(__file__)) + "Initialize A, B and C with values trained on efficientnet features." + s4d_A = np.load(curr_dir + "/s4d_A.dat.npy").flatten() + s4d_B = np.load(curr_dir + "/s4d_B.dat.npy").flatten() + s4d_C = np.load(curr_dir + "/s4d_C.dat.npy").flatten().flatten() + return s4d_A, s4d_B, s4d_C + + +def run_original_model( + input: np.ndarray, + num_steps: int, + model_dim: int, + d_states: int, + A: np.ndarray, + B: np.ndarray, + C: np.ndarray) -> Tuple[np.ndarray]: + """ + Run original S4d model. + + Parameters + ---------- + input: np.ndarray + Input signal to the model. + num_steps: int + Number of time steps to simulate the model. + model_dim: int + Dimensionality of the model. + d_states: int + Number of model states. + A: np.ndarray + Diagonal elements of the state matrix of the S4D model. + B: np.ndarray + Diagonal elements of the input matrix of the S4D model. + C: np.ndarray + Diagonal elements of the output matrix of the S4D model. + + Returns + ------- + Tuple[np.ndarray] + Tuple containing the output of the model simulation. + + Notes + ----- + This function simulates the behavior of a linear time-invariant system + with diagonalized state-space representation. + The state-space equations are given by: + x_{k+1} = A * x_k + B * u_k + y_k = C * x_k + + where: + - x_k is the state vector at time step k, + - u_k is the input vector at time step k, + - y_k is the output vector at time step k, + - A is the diagonal state matrix, + - B is the diagonal input matrix, + - C is the diagonal output matrix. + + The function computes the output of the system for the given input signal + over num_steps time steps. + """ + + A = A[:model_dim * d_states] + B = B[:model_dim * d_states] + C = C[:model_dim * d_states] + expansion_weights = np.kron(np.eye(model_dim), np.ones(d_states)) + expanded_inp = np.matmul(expansion_weights.T, input) + out = np.zeros((model_dim * d_states, num_steps)) + S4state = np.zeros((model_dim * d_states,)).flatten() + + for idx, inp in enumerate(expanded_inp.T): + S4state = np.multiply(S4state, A) + np.multiply(inp, B) + out[:, idx] = np.multiply(C, S4state) * 2 + + out = np.matmul(expansion_weights, out) + return out From dba843f35f4d37a27e2caf2e330227b32d7330c4 Mon Sep 17 00:00:00 2001 From: smm-ncl Date: Thu, 18 Jan 2024 02:43:44 -0800 Subject: [PATCH 3/7] update license --- tests/lava/proc/s4d/test_models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/lava/proc/s4d/test_models.py b/tests/lava/proc/s4d/test_models.py index 692f90df7..cc53e67e9 100644 --- a/tests/lava/proc/s4d/test_models.py +++ b/tests/lava/proc/s4d/test_models.py @@ -1,4 +1,5 @@ -# expressly stated in the License. +# Copyright (C) 2021-22 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause # See: https://spdx.org/licenses/ import unittest From dbb699ea7d1b2974bba6ad65d9a59c4bc5e48c21 Mon Sep 17 00:00:00 2001 From: smm-ncl Date: Thu, 18 Jan 2024 03:05:11 -0800 Subject: [PATCH 4/7] fix imports --- tests/lava/proc/s4d/test_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lava/proc/s4d/test_models.py b/tests/lava/proc/s4d/test_models.py index cc53e67e9..9d3da2f68 100644 --- a/tests/lava/proc/s4d/test_models.py +++ b/tests/lava/proc/s4d/test_models.py @@ -11,7 +11,7 @@ import lava.proc.io as io from typing import Tuple from lava.magma.core.run_configs import Loihi2SimCfg -from lava.tests.lava.proc.s4d.utils import get_coefficients, run_original_model +from tests.lava.proc.s4d.utils import get_coefficients, run_original_model class TestSigmaS4DDeltaModels(unittest.TestCase): From 446220634aa5412128502b6a03321daa3f93399c Mon Sep 17 00:00:00 2001 From: smm-ncl Date: Thu, 18 Jan 2024 03:17:22 -0800 Subject: [PATCH 5/7] linting --- src/lava/proc/s4d/models.py | 9 +++++---- src/lava/proc/s4d/process.py | 17 +++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/lava/proc/s4d/models.py b/src/lava/proc/s4d/models.py index bb0ed3086..5b9c64b8b 100644 --- a/src/lava/proc/s4d/models.py +++ b/src/lava/proc/s4d/models.py @@ -57,7 +57,7 @@ def activation_dynamics(self, sigma_data: np.ndarray) -> np.ndarray: Notes ----- - This function simulates the behavior of a linear time-invariant system + This function simulates the behavior of a linear time-invariant system with diagonalized state-space representation. (S4D) The state-space equations are given by: x_{k+1} = A * x_k + B * u_k @@ -71,7 +71,8 @@ def activation_dynamics(self, sigma_data: np.ndarray) -> np.ndarray: - B is the diagonal input matrix, - C is the diagonal output matrix. - The function computes the next output step of the system for the given input signal. + The function computes the next output step of the + system for the given input signal. """ self.S4state = self.S4state * self.A + sigma_data * self.B @@ -129,7 +130,7 @@ def __init__(self, proc): # Instantiate processes self.sparse1 = Sparse(weights=conn_weights.T, weight_exp=state_exp, - num_message_bits=num_message_bits) + num_message_bits=num_message_bits) self.sigmaS4delta = SigmaS4Delta(shape=(shape[0] * d_states,), vth=vth, state_exp=state_exp, @@ -138,7 +139,7 @@ def __init__(self, proc): B=B, C=C) self.sparse2 = Sparse(weights=conn_weights, weight_exp=state_exp, - num_message_bits=num_message_bits) + num_message_bits=num_message_bits) # Make connections Sparse -> SigmaS4Delta -> Sparse proc.in_ports.s_in.connect(self.sparse1.in_ports.s_in) diff --git a/src/lava/proc/s4d/process.py b/src/lava/proc/s4d/process.py index e400f31ec..cfffca593 100644 --- a/src/lava/proc/s4d/process.py +++ b/src/lava/proc/s4d/process.py @@ -20,7 +20,7 @@ def __init__( C: float, state_exp: ty.Optional[int] = 0, S4_exp: ty.Optional[int] = 0) -> None: - """ + """ Sigma delta neuron process that implements S4D dynamics as its activation function. @@ -47,7 +47,8 @@ def __init__( Notes ----- - This process simulates the behavior of a linear time-invariant system with state-space representation. + This process simulates the behavior of a linear time-invariant system + with diagonal state-space representation. The state-space equations are given by: x_{k+1} = A * x_k + B * u_k y_k = C * x_k @@ -90,7 +91,7 @@ def __init__( S4_exp: ty.Optional[int] = 0, num_message_bits: ty.Optional[int] = 24, state_exp: ty.Optional[int] = 0) -> None: - """ + """ Combines S4D neuron with Sparse Processes that allow for multiple d_states. @@ -107,7 +108,7 @@ def __init__( C: np.ndarray Diagonal elements of the output matrix of the S4D model. d_states: int - Number of hidden states of the S4D model. + Number of hidden states of the S4D model. Default is 1. state_exp: int Scaling exponent with base 2 for the reconstructed sigma variables. @@ -120,15 +121,15 @@ def __init__( Scaling exponent with base 2 for the S4 state variables. Note: Only relevant for nc model. Default is 0. - + Notes ----- - Connectivity: Sparse -> SigmaS4Delta -> Sparse. + Connectivity: Sparse -> SigmaS4Delta -> Sparse. Relieves user from computing required connection weights for multiple d_states. - This process simulates the behavior of a linear time-invariant system with - diagonalized state-space representation. (S4D) + This process simulates the behavior of a linear time-invariant system + with diagonalized state-space representation. (S4D) The state-space equations are given by: x_{k+1} = A * x_k + B * u_k y_k = C * x_k From c9f76468b6ace7fbf427e16a00ab3854969754bd Mon Sep 17 00:00:00 2001 From: smm-ncl Date: Tue, 23 Jan 2024 08:41:52 -0800 Subject: [PATCH 6/7] incorporate reviews --- src/lava/proc/s4d/models.py | 149 ++++++++++++++++------------ src/lava/proc/s4d/process.py | 142 ++++++++++++-------------- tests/lava/proc/s4d/test_models.py | 93 +++++++++-------- tests/lava/proc/s4d/test_process.py | 94 +++++++++--------- tests/lava/proc/s4d/utils.py | 72 +++++++------- 5 files changed, 287 insertions(+), 263 deletions(-) diff --git a/src/lava/proc/s4d/models.py b/src/lava/proc/s4d/models.py index 5b9c64b8b..fb97be9cb 100644 --- a/src/lava/proc/s4d/models.py +++ b/src/lava/proc/s4d/models.py @@ -1,13 +1,13 @@ -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # See: https://spdx.org/licenses/ -from lava.proc.sdn.models import AbstractSigmaDeltaModel +import numpy as np from typing import Any, Dict +from lava.proc.sdn.models import AbstractSigmaDeltaModel from lava.magma.core.decorator import implements, requires, tag -import numpy as np from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol -from lava.proc.s4d.process import SigmaS4Delta, SigmaS4DeltaLayer +from lava.proc.s4d.process import SigmaS4dDelta, SigmaS4dDeltaLayer from lava.magma.core.resources import CPU from lava.magma.core.model.py.ports import PyInPort, PyOutPort from lava.magma.core.model.py.type import LavaPyType @@ -15,7 +15,7 @@ from lava.proc.sparse.process import Sparse -class AbstractSigmaS4DeltaModel(AbstractSigmaDeltaModel): +class AbstractSigmaS4dDeltaModel(AbstractSigmaDeltaModel): a_in = None s_out = None @@ -29,62 +29,87 @@ class AbstractSigmaS4DeltaModel(AbstractSigmaDeltaModel): bias = None # S4 Variables - A = None - B = None - C = None - S4state = None - S4_exp = None + a = None + b = None + c = None + s4_state = None + s4_exp = None def __init__(self, proc_params: Dict[str, Any]) -> None: - super().__init__(proc_params) - self.A = self.proc_params['A'] - self.B = self.proc_params['B'] - self.C = self.proc_params['C'] - self.S4state = self.proc_params['S4state'] - - def activation_dynamics(self, sigma_data: np.ndarray) -> np.ndarray: - """Sigma Delta activation dynamics. Performs S4D dynamics. + """ + Sigma delta neuron model that implements S4D + (as described by Gu et al., 2022) dynamics as its activation function. Parameters ---------- - sigma_data: np.ndarray - sigma decoded data + shape: Tuple + Shape of the sigma process. + vth: int or float + Threshold of the delta encoder. + a: np.ndarray + Diagonal elements of the state matrix of the S4D model. + b: np.ndarray + Diagonal elements of the input matrix of the S4D model. + c: np.ndarray + Diagonal elements of the output matrix of the S4D model. + state_exp: int + Scaling exponent with base 2 for the reconstructed sigma variables. + Note: This should only be used for nc models. + Default is 0. + s4_exp: int + Scaling exponent with base 2 for the S4 state variables. + Note: This should only be used for nc models. + Default is 0. + """ + super().__init__(proc_params) + self.a = self.proc_params['a'] + self.b = self.proc_params['b'] + self.c = self.proc_params['c'] + self.s4_state = self.proc_params['s4_state'] - Returns - ------- - np.ndarray - activation output + def activation_dynamics(self, sigma_data: np.ndarray) -> np.ndarray: + """Sigma Delta activation dynamics. Performs S4D dynamics. - Notes - ----- This function simulates the behavior of a linear time-invariant system - with diagonalized state-space representation. (S4D) + with diagonalized state-space representation. + (For reference see Gu et al., 2022) + The state-space equations are given by: - x_{k+1} = A * x_k + B * u_k - y_k = C * x_k + s4_state_{k+1} = A * s4_state_k + B * input_k + act_k = C * s4_state_k where: - - x_k is the state vector at time step k, - - u_k is the input vector at time step k, - - y_k is the output vector at time step k, + - s4_state_k is the state vector at time step k, + - input_k is the input vector at time step k, + - act_k is the output vector at time step k, - A is the diagonal state matrix, - B is the diagonal input matrix, - C is the diagonal output matrix. The function computes the next output step of the system for the given input signal. + + Parameters + ---------- + sigma_data: np.ndarray + sigma decoded data + + Returns + ------- + np.ndarray + activation output """ - self.S4state = self.S4state * self.A + sigma_data * self.B - act = self.C * self.S4state * 2 + self.s4_state = self.s4_state * self.a + sigma_data * self.b + act = self.c * self.s4_state * 2 return act -@implements(proc=SigmaS4Delta, protocol=LoihiProtocol) +@implements(proc=SigmaS4dDelta, protocol=LoihiProtocol) @requires(CPU) @tag('floating_pt') -class PySigmaS4DeltaModelFloat(AbstractSigmaS4DeltaModel): - """Floating point implementation of SigmaS4Delta neuron.""" +class PySigmaS4dDeltaModelFloat(AbstractSigmaS4dDeltaModel): + """Floating point implementation of SigmaS4dDelta neuron.""" a_in = LavaPyType(PyInPort.VEC_DENSE, float) s_out = LavaPyType(PyOutPort.VEC_DENSE, float) @@ -98,13 +123,13 @@ class PySigmaS4DeltaModelFloat(AbstractSigmaS4DeltaModel): state_exp: np.ndarray = LavaPyType(np.ndarray, np.int32, precision=3) cum_error: np.ndarray = LavaPyType(np.ndarray, bool, precision=1) spike_exp: np.ndarray = LavaPyType(np.ndarray, np.int32, precision=3) - S4_exp: np.ndarray = LavaPyType(np.ndarray, np.int32, precision=3) + s4_exp: np.ndarray = LavaPyType(np.ndarray, np.int32, precision=3) - # S4 stuff - S4state: np.ndarray = LavaPyType(np.ndarray, float) - A: np.ndarray = LavaPyType(np.ndarray, float) - B: np.ndarray = LavaPyType(np.ndarray, float) - C: np.ndarray = LavaPyType(np.ndarray, float) + # S4 vaiables + s4_state: np.ndarray = LavaPyType(np.ndarray, float) + a: np.ndarray = LavaPyType(np.ndarray, float) + b: np.ndarray = LavaPyType(np.ndarray, float) + c: np.ndarray = LavaPyType(np.ndarray, float) def run_spk(self) -> None: # Receive synaptic input @@ -113,7 +138,7 @@ def run_spk(self) -> None: self.s_out.send(s_out) -@implements(proc=SigmaS4DeltaLayer, protocol=LoihiProtocol) +@implements(proc=SigmaS4dDeltaLayer, protocol=LoihiProtocol) class SubDenseLayerModel(AbstractSubProcessModel): def __init__(self, proc): """Builds (Sparse -> S4D -> Sparse) connection of the process.""" @@ -121,34 +146,34 @@ def __init__(self, proc): shape = proc.proc_params.get("shape") state_exp = proc.proc_params.get("state_exp") num_message_bits = proc.proc_params.get("num_message_bits") - S4_exp = proc.proc_params.get("S4_exp") + s4_exp = proc.proc_params.get("s4_exp") d_states = proc.proc_params.get("d_states") - A = proc.proc_params.get("A") - B = proc.proc_params.get("B") - C = proc.proc_params.get("C") + a = proc.proc_params.get("a") + b = proc.proc_params.get("b") + c = proc.proc_params.get("c") vth = proc.proc_params.get("vth") # Instantiate processes self.sparse1 = Sparse(weights=conn_weights.T, weight_exp=state_exp, num_message_bits=num_message_bits) - self.sigmaS4delta = SigmaS4Delta(shape=(shape[0] * d_states,), - vth=vth, - state_exp=state_exp, - S4_exp=S4_exp, - A=A, - B=B, - C=C) + self.sigma_S4d_delta = SigmaS4dDelta(shape=(shape[0] * d_states,), + vth=vth, + state_exp=state_exp, + s4_exp=s4_exp, + a=a, + b=b, + c=c) self.sparse2 = Sparse(weights=conn_weights, weight_exp=state_exp, num_message_bits=num_message_bits) # Make connections Sparse -> SigmaS4Delta -> Sparse proc.in_ports.s_in.connect(self.sparse1.in_ports.s_in) - self.sparse1.out_ports.a_out.connect(self.sigmaS4delta.in_ports.a_in) - self.sigmaS4delta.out_ports.s_out.connect(self.sparse2.s_in) + self.sparse1.out_ports.a_out.connect(self.sigma_S4d_delta.in_ports.a_in) + self.sigma_S4d_delta.out_ports.s_out.connect(self.sparse2.s_in) self.sparse2.out_ports.a_out.connect(proc.out_ports.a_out) - # Set aliasses - proc.vars.A.alias(self.sigmaS4delta.vars.A) - proc.vars.B.alias(self.sigmaS4delta.vars.B) - proc.vars.C.alias(self.sigmaS4delta.vars.C) - proc.vars.S4state.alias(self.sigmaS4delta.vars.S4state) + # Set aliases + proc.vars.a.alias(self.sigma_S4d_delta.vars.a) + proc.vars.b.alias(self.sigma_S4d_delta.vars.b) + proc.vars.c.alias(self.sigma_S4d_delta.vars.c) + proc.vars.s4_state.alias(self.sigma_S4d_delta.vars.s4_state) diff --git a/src/lava/proc/s4d/process.py b/src/lava/proc/s4d/process.py index cfffca593..218e7292c 100644 --- a/src/lava/proc/s4d/process.py +++ b/src/lava/proc/s4d/process.py @@ -1,28 +1,42 @@ -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2024 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 from lava.proc.sdn.process import ActivationMode, SigmaDelta -import typing as ty -import numpy as np -class SigmaS4Delta(SigmaDelta, AbstractProcess): +class SigmaS4dDelta(SigmaDelta, AbstractProcess): def __init__( self, shape: ty.Tuple[int, ...], vth: ty.Union[int, float], - A: float, - B: float, - C: float, + a: float, + b: float, + c: float, state_exp: ty.Optional[int] = 0, - S4_exp: ty.Optional[int] = 0) -> None: + s4_exp: ty.Optional[int] = 0) -> None: """ - Sigma delta neuron process that implements S4D dynamics as its - activation function. + Sigma delta neuron process that implements S4D (described by + Gu et al., 2022) dynamics as its activation function. + + This process simulates the behavior of a linear time-invariant system + with diagonal state-space representation. + The state-space equations are given by: + s4_state_{k+1} = A * s4_state_k + B * inp_k + act_k = C * s4_state_k + + where: + - s4_state_k is the state vector at time step k, + - inp_k is the input vector at time step k, + - act_k is the output vector at time step k, + - A is the diagonal state matrix, + - B is the diagonal input matrix, + - C is the diagonal output matrix. Parameters ---------- @@ -30,82 +44,70 @@ def __init__( Shape of the sigma process. vth: int or float Threshold of the delta encoder. - A: np.ndarray + a: np.ndarray Diagonal elements of the state matrix of the S4D model. - B: np.ndarray + b: np.ndarray Diagonal elements of the input matrix of the S4D model. - C: np.ndarray + c: np.ndarray Diagonal elements of the output matrix of the S4D model. state_exp: int Scaling exponent with base 2 for the reconstructed sigma variables. Note: This should only be used for nc models. Default is 0. - S4_exp: int + s4_exp: int Scaling exponent with base 2 for the S4 state variables. Note: This should only be used for nc models. Default is 0. - - Notes - ----- - This process simulates the behavior of a linear time-invariant system - with diagonal state-space representation. - The state-space equations are given by: - x_{k+1} = A * x_k + B * u_k - y_k = C * x_k - - where: - - x_k is the state vector at time step k, - - u_k is the input vector at time step k, - - y_k is the output vector at time step k, - - A is the diagonal state matrix, - - B is the diagonal input matrix, - - C is the diagonal output matrix. """ super().__init__(shape=shape, vth=vth, - A=A, - B=B, - C=C, - S4state=0, + a=a, + b=b, + c=c, + s4_state=0, state_exp=state_exp, - S4_exp=S4_exp) + s4_exp=s4_exp) # Variables for S4 - self.A = Var(shape=shape, init=A) - self.B = Var(shape=shape, init=B) - self.C = Var(shape=shape, init=C) - self.S4state = Var(shape=shape, init=0) - self.S4_exp = Var(shape=(1,), init=S4_exp) + self.a = Var(shape=shape, init=a) + self.b = Var(shape=shape, init=b) + self.c = Var(shape=shape, init=c) + self.s4_state = Var(shape=shape, init=0) + self.s4_exp = Var(shape=(1,), init=s4_exp) -class SigmaS4DeltaLayer(AbstractProcess): +class SigmaS4dDeltaLayer(AbstractProcess): def __init__( self, shape: ty.Tuple[int, ...], vth: ty.Union[int, float], - A: float, - B: float, - C: float, + a: float, + b: float, + c: float, d_states: ty.Optional[int] = 1, - S4_exp: ty.Optional[int] = 0, + s4_exp: ty.Optional[int] = 0, num_message_bits: ty.Optional[int] = 24, state_exp: ty.Optional[int] = 0) -> None: """ Combines S4D neuron with Sparse Processes that allow for multiple d_states. + Connectivity: Sparse -> SigmaS4dDelta -> Sparse. + Relieves user from computing required connection weights for multiple + d_states. + Parameters ---------- shape: Tuple Shape of the sigma process. vth: int or float Threshold of the delta encoder. - A: np.ndarray + a: np.ndarray Diagonal elements of the state matrix of the S4D model. - B: np.ndarray + b: np.ndarray Diagonal elements of the input matrix of the S4D model. - C: np.ndarray + c: np.ndarray Diagonal elements of the output matrix of the S4D model. d_states: int Number of hidden states of the S4D model. @@ -117,43 +119,23 @@ def __init__( num_message_bits: int Number of message bits to be used in Sparse connection processes. Note: Only relevant for nc model. - S4_exp: int + s4_exp: int Scaling exponent with base 2 for the S4 state variables. Note: Only relevant for nc model. Default is 0. - - Notes - ----- - Connectivity: Sparse -> SigmaS4Delta -> Sparse. - Relieves user from computing required connection weights for multiple - d_states. - - This process simulates the behavior of a linear time-invariant system - with diagonalized state-space representation. (S4D) - The state-space equations are given by: - x_{k+1} = A * x_k + B * u_k - y_k = C * x_k - - where: - - x_k is the state vector at time step k, - - u_k is the input vector at time step k, - - y_k is the output vector at time step k, - - A is the diagonal state matrix, - - B is the diagonal input matrix, - - C is the diagonal output matrix. """ # Automatically takes care of expansion and reduction of dimensionality # for multiple hidden states (d_states) conn_weights = np.kron(np.eye(shape[0]), np.ones(d_states)) - S4state = 0 + s4_state = 0 super().__init__(shape=shape, vth=vth, - A=A, - B=B, - C=C, - S4_exp=S4_exp, - S4state=S4state, + a=a, + b=b, + c=c, + s4_exp=s4_exp, + s4_state=s4_state, conn_weights=conn_weights, num_message_bits=num_message_bits, d_states=d_states, @@ -168,11 +150,11 @@ def __init__( self.state_exp = Var(shape=(1,), init=state_exp) # Variables for S4 - self.A = Var(shape=(shape[0] * d_states,), init=A) - self.B = Var(shape=(shape[0] * d_states,), init=B) - self.C = Var(shape=(shape[0] * d_states,), init=C) - self.S4state = Var(shape=(shape[0] * d_states,), init=0) - self.S4_exp = Var(shape=(1,), init=S4_exp) + self.a = Var(shape=(shape[0] * d_states,), init=a) + self.b = Var(shape=(shape[0] * d_states,), init=b) + self.c = Var(shape=(shape[0] * d_states,), init=c) + self.s4_state = Var(shape=(shape[0] * d_states,), init=0) + self.S4_exp = Var(shape=(1,), init=s4_exp) # Variables for connecting Dense processes # Project input_dim to input_dim * d_states diff --git a/tests/lava/proc/s4d/test_models.py b/tests/lava/proc/s4d/test_models.py index 9d3da2f68..c08e19cf8 100644 --- a/tests/lava/proc/s4d/test_models.py +++ b/tests/lava/proc/s4d/test_models.py @@ -1,15 +1,15 @@ -# Copyright (C) 2021-22 Intel Corporation +# Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # See: https://spdx.org/licenses/ import unittest import numpy as np +from typing import Tuple +import lava.proc.io as io from lava.magma.core.run_conditions import RunSteps from lava.proc.sdn.process import ActivationMode, SigmaDelta -from lava.proc.s4d.process import SigmaS4Delta, SigmaS4DeltaLayer +from lava.proc.s4d.process import SigmaS4dDelta, SigmaS4dDeltaLayer from lava.proc.sparse.process import Sparse -import lava.proc.io as io -from typing import Tuple from lava.magma.core.run_configs import Loihi2SimCfg from tests.lava.proc.s4d.utils import get_coefficients, run_original_model @@ -18,7 +18,10 @@ class TestSigmaS4DDeltaModels(unittest.TestCase): """Tests for SigmaS4Delta neuron""" def run_in_lava( self, - input, + inp, + a: np.ndarray, + b: np.ndarray, + c: np.ndarray, num_steps: int, model_dim: int, d_states: int, @@ -28,7 +31,7 @@ def run_in_lava( Parameters ---------- - input : np.ndarray + inp : np.ndarray Input signal to the model. num_steps : int Number of time steps to simulate the model. @@ -47,24 +50,24 @@ def run_in_lava( Tuple containing the output of the model simulation. """ - A = self.A[:model_dim * d_states] - B = self.B[:model_dim * d_states] - C = self.C[:model_dim * d_states] + a = a[:model_dim * d_states] + b = b[:model_dim * d_states] + c = c[:model_dim * d_states] - diff = input[:, 1:] - input[:, :-1] - diff = np.concatenate((input[:, :1], diff), axis=1) + diff = inp[:, 1:] - inp[:, :-1] + diff = np.concatenate((inp[:, :1], diff), axis=1) spiker = io.source.RingBuffer(data=diff) receiver = io.sink.RingBuffer(shape=(model_dim,), buffer=num_steps) if use_layer: - s4d_layer = SigmaS4DeltaLayer(shape=(model_dim,), - d_states=d_states, - num_message_bits=24, - vth=0, - A=A, - B=B, - C=C) + s4d_layer = SigmaS4dDeltaLayer(shape=(model_dim,), + d_states=d_states, + num_message_bits=24, + vth=0, + a=a, + b=b, + c=c) buffer_neuron = SigmaDelta(shape=(model_dim,), vth=0, cum_error=True, @@ -75,11 +78,11 @@ def run_in_lava( else: sparse = Sparse(weights=np.eye(model_dim), num_message_bits=24) - s4d_neuron = SigmaS4Delta(shape=((model_dim,)), - vth=0, - A=A, - B=B, - C=C) + s4d_neuron = SigmaS4dDelta(shape=((model_dim,)), + vth=0, + a=a, + b=b, + c=c) spiker.s_out.connect(sparse.s_in) sparse.a_out.connect(s4d_neuron.a_in) s4d_neuron.s_out.connect(receiver.a_in) @@ -96,29 +99,32 @@ def run_in_lava( return output def test_py_model_vs_original_equations(self) -> None: - """Tests that the pymodel for SigmaS4Delta outputs approximately + """Tests that the pymodel for SigmaS4dDelta outputs approximately the same values as the original S4D equations. """ - self.A, self.B, self.C = get_coefficients() + a, b, c = get_coefficients() model_dim = 3 d_states = 1 n_steps = 5 np.random.seed(0) inp = np.random.random((model_dim, n_steps)) * 2**6 - out_chip = self.run_in_lava(input=inp, + out_chip = self.run_in_lava(inp=inp, + a=a, + b=b, + c=c, num_steps=n_steps, model_dim=model_dim, d_states=d_states, use_layer=False ) - out_original_model = run_original_model(input=inp, + out_original_model = run_original_model(inp=inp, model_dim=model_dim, d_states=d_states, num_steps=n_steps, - A=self.A, - B=self.B, - C=self.C) + a=a, + b=b, + c=c) np.testing.assert_array_equal(out_original_model[:, :-1], out_chip[:, 1:]) @@ -127,26 +133,29 @@ def test_py_model_layer_vs_original_equations(self) -> None: """ Tests that the pymodel for SigmaS4DeltaLayer outputs approximately the same values as the original S4D equations for multiple d_states. """ - self.A, self.B, self.C = get_coefficients() + a, b, c = get_coefficients() model_dim = 3 d_states = 3 n_steps = 5 np.random.seed(1) inp = np.random.random((model_dim, n_steps)) * 2**6 - out_chip = self.run_in_lava(input=inp, + out_chip = self.run_in_lava(inp=inp, + a=a, + b=b, + c=c, num_steps=n_steps, model_dim=model_dim, d_states=d_states, use_layer=True, ) - out_original_model = run_original_model(input=inp, + out_original_model = run_original_model(inp=inp, model_dim=model_dim, d_states=d_states, num_steps=n_steps, - A=self.A, - B=self.B, - C=self.C) + a=a, + b=b, + c=c) np.testing.assert_allclose(out_original_model[:, :-2], out_chip[:, 2:]) @@ -154,20 +163,26 @@ def test_py_model_vs_py_model_layer(self) -> None: """Tests that the pymodel for SigmaS4DeltaLayer outputs approximately the same values as just the SigmaS4DDelta Model with one hidden dim. """ - self.A, self.B, self.C = get_coefficients() + a, b, c = get_coefficients() model_dim = 3 d_states = 1 n_steps = 5 np.random.seed(2) inp = np.random.random((model_dim, n_steps)) * 2**6 - out_just_model = self.run_in_lava(input=inp, + out_just_model = self.run_in_lava(inp=inp, + a=a, + b=b, + c=c, num_steps=n_steps, model_dim=model_dim, d_states=d_states, use_layer=False) - out_layer = self.run_in_lava(input=inp, + out_layer = self.run_in_lava(inp=inp, + a=a, + b=b, + c=c, num_steps=n_steps, model_dim=model_dim, d_states=d_states, diff --git a/tests/lava/proc/s4d/test_process.py b/tests/lava/proc/s4d/test_process.py index 4eddfb92d..5f6cbcc9c 100644 --- a/tests/lava/proc/s4d/test_process.py +++ b/tests/lava/proc/s4d/test_process.py @@ -1,81 +1,81 @@ -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # See: https://spdx.org/licenses/ import unittest -from lava.proc.s4d.process import SigmaS4Delta, SigmaS4DeltaLayer import numpy as np +from lava.proc.s4d.process import SigmaS4dDelta, SigmaS4dDeltaLayer -class TestSigmaS4DeltaProcess(unittest.TestCase): - """Tests for SigmaS4Delta Class""" +class TestSigmaS4dDeltaProcess(unittest.TestCase): + """Tests for SigmaS4dDelta Class""" def test_init(self) -> None: - """Tests instantiation of SigmaS4Delta""" + """Tests instantiation of SigmaS4dDelta""" shape = 10 vth = 10 state_exp = 6 s4_exp = 12 - A = np.ones(shape) * 0.5 - B = np.ones(shape) * 0.8 - C = np.ones(shape) * 0.9 - sigmas4delta = SigmaS4Delta(shape=(shape,), - vth=vth, - state_exp=state_exp, - S4_exp=s4_exp, - A=A, - B=B, - C=C) + a = np.ones(shape) * 0.5 + b = np.ones(shape) * 0.8 + c = np.ones(shape) * 0.9 + sigma_s4_delta = SigmaS4dDelta(shape=(shape,), + vth=vth, + state_exp=state_exp, + s4_exp=s4_exp, + a=a, + b=b, + c=c) # determined by user - S4 part - self.assertEqual(sigmas4delta.shape, (shape,)) - self.assertEqual(sigmas4delta.vth.init, vth * 2 ** state_exp) - self.assertEqual(sigmas4delta.S4_exp.init, s4_exp) - np.testing.assert_array_equal(sigmas4delta.A.init, A) - np.testing.assert_array_equal(sigmas4delta.B.init, B) - np.testing.assert_array_equal(sigmas4delta.C.init, C) - self.assertEqual(sigmas4delta.state_exp.init, state_exp) - self.assertEqual(sigmas4delta.S4state.init, 0) + self.assertEqual(sigma_s4_delta.shape, (shape,)) + self.assertEqual(sigma_s4_delta .vth.init, vth * 2 ** state_exp) + self.assertEqual(sigma_s4_delta.s4_exp.init, s4_exp) + np.testing.assert_array_equal(sigma_s4_delta.a.init, a) + np.testing.assert_array_equal(sigma_s4_delta.b.init, b) + np.testing.assert_array_equal(sigma_s4_delta.c.init, c) + self.assertEqual(sigma_s4_delta.state_exp.init, state_exp) + self.assertEqual(sigma_s4_delta.s4_state.init, 0) # default sigmadelta params - inherited from SigmaDelta class - self.assertEqual(sigmas4delta.cum_error.init, False) - self.assertEqual(sigmas4delta.spike_exp.init, 0) - self.assertEqual(sigmas4delta.bias.init, 0) + self.assertEqual(sigma_s4_delta.cum_error.init, False) + self.assertEqual(sigma_s4_delta.spike_exp.init, 0) + self.assertEqual(sigma_s4_delta.bias.init, 0) class TestSigmaS4DeltaLayer(unittest.TestCase): - """Tests for SigmaS4DeltaLayer Class""" + """Tests for SigmaS4dDeltaLayer Class""" def test_init(self) -> None: - """Tests instantiation of SigmaS4DeltaLayer """ + """Tests instantiation of SigmaS4dDeltaLayer """ shape = 10 vth = 10 state_exp = 6 s4_exp = 12 d_states = 5 - A = np.ones(shape * d_states) * 0.5 - B = np.ones(shape * d_states) * 0.8 - C = np.ones(shape * d_states) * 0.9 + a = np.ones(shape) * 0.5 + b = np.ones(shape) * 0.8 + c = np.ones(shape) * 0.9 - sigmas4deltalayer = SigmaS4DeltaLayer(shape=(shape,), - d_states=d_states, - vth=vth, - state_exp=state_exp, - S4_exp=s4_exp, - A=A, - B=B, - C=C) + sigma_s4d_delta_layer = SigmaS4dDeltaLayer(shape=(shape,), + d_states=d_states, + vth=vth, + state_exp=state_exp, + s4_exp=s4_exp, + a=a, + b=b, + c=c) # determined by user - S4 part - self.assertEqual(sigmas4deltalayer.shape, (shape,)) - self.assertEqual(sigmas4deltalayer.S4_exp.init, s4_exp) - np.testing.assert_array_equal(sigmas4deltalayer.A.init, A) - np.testing.assert_array_equal(sigmas4deltalayer.B.init, B) - np.testing.assert_array_equal(sigmas4deltalayer.C.init, C) - self.assertEqual(sigmas4deltalayer.state_exp.init, state_exp) - self.assertEqual(sigmas4deltalayer.S4state.init, 0) + self.assertEqual(sigma_s4d_delta_layer.shape, (shape,)) + self.assertEqual(sigma_s4d_delta_layer.S4_exp.init, s4_exp) + np.testing.assert_array_equal(sigma_s4d_delta_layer.a.init, a) + np.testing.assert_array_equal(sigma_s4d_delta_layer.b.init, b) + np.testing.assert_array_equal(sigma_s4d_delta_layer.c.init, c) + self.assertEqual(sigma_s4d_delta_layer.state_exp.init, state_exp) + self.assertEqual(sigma_s4d_delta_layer.s4_state.init, 0) # determined by user/via number of states and shape - np.testing.assert_array_equal(sigmas4deltalayer.conn_weights.init, + np.testing.assert_array_equal(sigma_s4d_delta_layer.conn_weights.init, np.kron(np.eye(shape), np.ones(d_states))) diff --git a/tests/lava/proc/s4d/utils.py b/tests/lava/proc/s4d/utils.py index f649c8bd1..3afd48b2e 100644 --- a/tests/lava/proc/s4d/utils.py +++ b/tests/lava/proc/s4d/utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # See: https://spdx.org/licenses/ @@ -9,7 +9,8 @@ def get_coefficients() -> [np.ndarray, np.ndarray, np.ndarray]: curr_dir = os.path.dirname(os.path.realpath(__file__)) - "Initialize A, B and C with values trained on efficientnet features." + + # Initialize A, B and C with values trained on efficientnet features. s4d_A = np.load(curr_dir + "/s4d_A.dat.npy").flatten() s4d_B = np.load(curr_dir + "/s4d_B.dat.npy").flatten() s4d_C = np.load(curr_dir + "/s4d_C.dat.npy").flatten().flatten() @@ -17,16 +18,36 @@ def get_coefficients() -> [np.ndarray, np.ndarray, np.ndarray]: def run_original_model( - input: np.ndarray, + inp: np.ndarray, num_steps: int, model_dim: int, d_states: int, - A: np.ndarray, - B: np.ndarray, - C: np.ndarray) -> Tuple[np.ndarray]: + a: np.ndarray, + b: np.ndarray, + c: np.ndarray) -> Tuple[np.ndarray]: """ Run original S4d model. + This function simulates the behavior of a linear time-invariant system + with diagonalized state-space representation. (S4D) + The state-space equations are given by: + s4_state_{k+1} = A * s4_state_k + B * input_k + out_k = C * s4_state_k + + where: + - s4_state_k is the state vector at time step k, + - input_k is the input vector at time step k, + - out_k is the output vector at time step k, + - A is the diagonal state matrix, + - B is the diagonal input matrix, + - C is the diagonal output matrix. + + The function computes the next output step of the + system for the given input signal. + + The function computes the output of the system for the given input signal + over num_steps time steps. + Parameters ---------- input: np.ndarray @@ -37,49 +58,30 @@ def run_original_model( Dimensionality of the model. d_states: int Number of model states. - A: np.ndarray + a: np.ndarray Diagonal elements of the state matrix of the S4D model. - B: np.ndarray + b: np.ndarray Diagonal elements of the input matrix of the S4D model. - C: np.ndarray + c: np.ndarray Diagonal elements of the output matrix of the S4D model. Returns ------- Tuple[np.ndarray] Tuple containing the output of the model simulation. - - Notes - ----- - This function simulates the behavior of a linear time-invariant system - with diagonalized state-space representation. - The state-space equations are given by: - x_{k+1} = A * x_k + B * u_k - y_k = C * x_k - - where: - - x_k is the state vector at time step k, - - u_k is the input vector at time step k, - - y_k is the output vector at time step k, - - A is the diagonal state matrix, - - B is the diagonal input matrix, - - C is the diagonal output matrix. - - The function computes the output of the system for the given input signal - over num_steps time steps. """ - A = A[:model_dim * d_states] - B = B[:model_dim * d_states] - C = C[:model_dim * d_states] + a = a[:model_dim * d_states] + b = b[:model_dim * d_states] + c = c[:model_dim * d_states] expansion_weights = np.kron(np.eye(model_dim), np.ones(d_states)) - expanded_inp = np.matmul(expansion_weights.T, input) + expanded_inp = np.matmul(expansion_weights.T, inp) out = np.zeros((model_dim * d_states, num_steps)) - S4state = np.zeros((model_dim * d_states,)).flatten() + s4_state = np.zeros((model_dim * d_states,)).flatten() for idx, inp in enumerate(expanded_inp.T): - S4state = np.multiply(S4state, A) + np.multiply(inp, B) - out[:, idx] = np.multiply(C, S4state) * 2 + s4_state = np.multiply(s4_state, a) + np.multiply(inp, b) + out[:, idx] = np.multiply(c, s4_state) * 2 out = np.matmul(expansion_weights, out) return out From 6138d592c26d2ad299670f05d333d0756a30544c Mon Sep 17 00:00:00 2001 From: smm-ncl Date: Tue, 23 Jan 2024 08:54:20 -0800 Subject: [PATCH 7/7] update docstring --- src/lava/proc/s4d/models.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/lava/proc/s4d/models.py b/src/lava/proc/s4d/models.py index fb97be9cb..c409ba3af 100644 --- a/src/lava/proc/s4d/models.py +++ b/src/lava/proc/s4d/models.py @@ -40,26 +40,16 @@ def __init__(self, proc_params: Dict[str, Any]) -> None: Sigma delta neuron model that implements S4D (as described by Gu et al., 2022) dynamics as its activation function. - Parameters - ---------- - shape: Tuple - Shape of the sigma process. - vth: int or float - Threshold of the delta encoder. + Relevant parameters in proc_params + -------------------------- a: np.ndarray Diagonal elements of the state matrix of the S4D model. b: np.ndarray Diagonal elements of the input matrix of the S4D model. c: np.ndarray Diagonal elements of the output matrix of the S4D model. - state_exp: int - Scaling exponent with base 2 for the reconstructed sigma variables. - Note: This should only be used for nc models. - Default is 0. - s4_exp: int - Scaling exponent with base 2 for the S4 state variables. - Note: This should only be used for nc models. - Default is 0. + s4_state: np.ndarray + State vector of the S4D model. """ super().__init__(proc_params) self.a = self.proc_params['a']