From d3a65e7e860feb2c90794ff865934e5e7ffdce90 Mon Sep 17 00:00:00 2001 From: bamsumit Date: Thu, 20 Jan 2022 16:26:51 -0800 Subject: [PATCH 01/21] permute initial implementation Signed-off-by: bamsumit --- .../magma/core/process/ports/exceptions.py | 14 +++++ src/lava/magma/core/process/ports/ports.py | 55 ++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/lava/magma/core/process/ports/exceptions.py b/src/lava/magma/core/process/ports/exceptions.py index 1c1a96de0..60bb5b943 100644 --- a/src/lava/magma/core/process/ports/exceptions.py +++ b/src/lava/magma/core/process/ports/exceptions.py @@ -34,6 +34,20 @@ def __init__(self, shapes, axis): super().__init__(self, msg) +class PermuteError(Exception): + """Raised when permute order is incompatible with old shape dimension.""" + + def __init__( + self, old_shape: ty.Tuple, order: ty.Union[ty.Tuple, ty.List] + ) -> None: + msg = ( + "Cannot permute 'old_shape'={} with permutation 'order={}. " + "Total number of dimensions must not change during " + "reshaping.".format(old_shape, order) + ) + super().__init__(msg) + + class VarNotSharableError(Exception): """Raised when an attempt is made to connect a RefPort or VarPort to a non-sharable Var.""" diff --git a/src/lava/magma/core/process/ports/ports.py b/src/lava/magma/core/process/ports/ports.py index 82a6ce39e..6ea2dc50b 100644 --- a/src/lava/magma/core/process/ports/ports.py +++ b/src/lava/magma/core/process/ports/ports.py @@ -204,6 +204,28 @@ def concat_with( self._validate_ports(ports, port_type, assert_same_shape=False) return ConcatPort(ports, axis) + def permute( + self, + order: ty.Union[ty.Tuple, ty.List] + ) -> "PermutePort": + """Permutes the tensor dimensio of this port by deriving and returning + a new virtual PermutePort the new permuted dimension. This implies that + the resulting PermutePort can only be forward connected to another port. + + Parameters + ---------- + :param order: Order of permutation. Number of total elements and number + of dimensions must not change. + """ + if len(self.shape) != len(order): + raise pe.PermuteError(self.shape, order) + + permute_port = PermutePort(tuple([self.shape[i] for i in order])) + self._connect_forward( + [permute_port], AbstractPort, assert_same_shape=False + ) + return permute_port + class AbstractIOPort(AbstractPort): """Abstract base class for InPorts and OutPorts. @@ -701,7 +723,38 @@ class PermutePort(AbstractPort, AbstractVirtualPort): out_port.permute([3, 1, 2]).connect(in_port) """ - pass + def __init__(self, shape: ty.Tuple): + AbstractPort.__init__(self, shape) + + @property + def _parent_port(self) -> AbstractPort: + return self.in_connections[0] + + def connect(self, ports: ty.Union["AbstractPort", ty.List["AbstractPort"]]): + """Connects this ReshapePort to other port(s). + + Parameters + ---------- + :param ports: The port(s) to connect to. Connections from an IOPort + to a RVPort and vice versa are not allowed. + """ + # Determine allows port_type + if isinstance(self._parent_port, OutPort): + # If OutPort, only allow other IO ports + port_type = AbstractIOPort + elif isinstance(self._parent_port, InPort): + # If InPort, only allow other InPorts + port_type = InPort + elif isinstance(self._parent_port, RefPort): + # If RefPort, only allow other Ref- or VarPorts + port_type = AbstractRVPort + elif isinstance(self._parent_port, VarPort): + # If VarPort, only allow other VarPorts + port_type = VarPort + else: + raise TypeError("Illegal parent port.") + # Connect to ports + self._connect_forward(to_list(ports), port_type) # ToDo: TBD... From 58d5a14845262f2f9bdf52bd1504ad35aa844f4e Mon Sep 17 00:00:00 2001 From: bamsumit Date: Tue, 25 Jan 2022 10:16:31 -0800 Subject: [PATCH 02/21] Tests for permute ports --- .../lava/magma/core/process/ports/__init__.py | 0 .../magma/core/process/ports/test_permute.py | 88 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 tests/lava/magma/core/process/ports/__init__.py create mode 100644 tests/lava/magma/core/process/ports/test_permute.py diff --git a/tests/lava/magma/core/process/ports/__init__.py b/tests/lava/magma/core/process/ports/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/lava/magma/core/process/ports/test_permute.py b/tests/lava/magma/core/process/ports/test_permute.py new file mode 100644 index 000000000..5fc140b9b --- /dev/null +++ b/tests/lava/magma/core/process/ports/test_permute.py @@ -0,0 +1,88 @@ +# Copyright (C) 2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# See: https://spdx.org/licenses/ +from typing import List +import unittest +import numpy as np + +from lava.magma.core.run_configs import RunConfig +from lava.magma.core.run_conditions import RunSteps +from lava.proc.io.source import RingBuffer as SendProcess +from lava.proc.io.sink import RingBuffer as ReceiveProcess +from lava.magma.core.model.py.model import PyLoihiProcessModel + +np.random.seed(7739) + + +class TestRunConfig(RunConfig): + """Run configuration selects appropriate ProcessModel based on tag + """ + def __init__(self, select_tag: str = 'fixed_pt'): + super(TestRunConfig, self).__init__(custom_sync_domains=None) + self.select_tag = select_tag + + def select( + self, _, proc_models: List[PyLoihiProcessModel] + ) -> PyLoihiProcessModel: + for pm in proc_models: + if self.select_tag in pm.tags: + return pm + raise AssertionError('No legal ProcessModel found.') + + +class TestSendReceive(unittest.TestCase): + """Tests for all SendProces and ReceiveProcess.""" + + def test_permute(self) -> None: + """Test whatever is being sent form source is received at sink.""" + num_steps = 10 + shape = (64, 32, 16) + input = np.random.randint(256, size=shape + (num_steps,)) + input -= 128 + # input = 0.5 * input + + source = SendProcess(data=input) + sink = ReceiveProcess(shape=shape[::-1], buffer=num_steps) + source.out_ports.s_out.permute([2, 1, 0]).connect(sink.in_ports.a_in) + + run_condition = RunSteps(num_steps=num_steps) + run_config = TestRunConfig(select_tag='floating_pt') + sink.run(condition=run_condition, run_cfg=run_config) + output = sink.data.get() + sink.stop() + + expected = input.permute([2, 1, 0]) + self.assertTrue( + np.all(output == expected), + f'Input and Ouptut do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + +if __name__ == '__main__': + num_steps = 10 + shape = (64, 32, 16) + input = np.random.randint(256, size=shape + (num_steps,)) + input -= 128 + # input = 0.5 * input + + source = SendProcess(data=input) + # sink = ReceiveProcess(shape=shape, buffer=num_steps) + # source.out_ports.s_out.connect(sink.in_ports.a_in) + + # sink = ReceiveProcess(shape=shape[::-1], buffer=num_steps) + # source.out_ports.s_out.permute([2, 1, 0]).connect(sink.in_ports.a_in) + + sink = ReceiveProcess(shape=(np.prod(shape), ), buffer=num_steps) + source.out_ports.s_out.flatten().connect(sink.in_ports.a_in) + + run_condition = RunSteps(num_steps=num_steps) + run_config = TestRunConfig(select_tag='floating_pt') + sink.run(condition=run_condition, run_cfg=run_config) + output = sink.data.get() + sink.stop() + + # expected = input.transpose([2, 1, 0, 3]) + expected = input.reshape([-1, num_steps]) + print(np.all(output == expected)) From 3891c8cb7b9008682f5d0520837f883954cc6071 Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Fri, 4 Feb 2022 19:17:45 +0100 Subject: [PATCH 03/21] Process property of virtual ports no longer returns None Signed-off-by: Mathis Richter --- src/lava/magma/core/process/ports/ports.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lava/magma/core/process/ports/ports.py b/src/lava/magma/core/process/ports/ports.py index 6ea2dc50b..4a1914202 100644 --- a/src/lava/magma/core/process/ports/ports.py +++ b/src/lava/magma/core/process/ports/ports.py @@ -601,7 +601,7 @@ def process(self): # ToDo: (AW) ReshapePort.connect(..) could be consolidated with # ConcatPort.connect(..) -class ReshapePort(AbstractPort, AbstractVirtualPort): +class ReshapePort(AbstractVirtualPort, AbstractPort): """A ReshapePort is a virtual port that allows to change the shape of a port before connecting to another port. It is used by the compiler to map the indices of the underlying @@ -641,7 +641,7 @@ def connect(self, ports: ty.Union["AbstractPort", ty.List["AbstractPort"]]): self._connect_forward(to_list(ports), port_type) -class ConcatPort(AbstractPort, AbstractVirtualPort): +class ConcatPort(AbstractVirtualPort, AbstractPort): """A ConcatPort is a virtual port that allows to concatenate multiple ports along given axis into a new port before connecting to another port. The shape of all concatenated ports outside of the concatenation @@ -711,7 +711,7 @@ def connect(self, ports: ty.Union["AbstractPort", ty.List["AbstractPort"]]): # ToDo: TBD... -class PermutePort(AbstractPort, AbstractVirtualPort): +class PermutePort(AbstractVirtualPort, AbstractPort): """A PermutePort is a virtual port that allows to permute the dimensions of a port before connecting to another port. It is used by the compiler to map the indices of the underlying @@ -758,7 +758,7 @@ def connect(self, ports: ty.Union["AbstractPort", ty.List["AbstractPort"]]): # ToDo: TBD... -class ReIndexPort(AbstractPort, AbstractVirtualPort): +class ReIndexPort(AbstractVirtualPort, AbstractPort): """A ReIndexPort is a virtual port that allows to re-index the elements of a port before connecting to another port. It is used by the compiler to map the indices of the underlying From eba5df91cac6195bbec6183dcc62fa7ae43ef4b2 Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Fri, 4 Feb 2022 22:12:35 +0100 Subject: [PATCH 04/21] Added initial run-unittest for flatten() from issue #163 Signed-off-by: Mathis Richter --- .../magma/core/process/ports/test_flatten.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/lava/magma/core/process/ports/test_flatten.py diff --git a/tests/lava/magma/core/process/ports/test_flatten.py b/tests/lava/magma/core/process/ports/test_flatten.py new file mode 100644 index 000000000..daa6f2160 --- /dev/null +++ b/tests/lava/magma/core/process/ports/test_flatten.py @@ -0,0 +1,48 @@ +# Copyright (C) 2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# See: https://spdx.org/licenses/ + +from typing import List +import numpy as np + +from lava.magma.core.run_configs import RunConfig +from lava.magma.core.run_conditions import RunSteps +from lava.proc.io.source import RingBuffer as SendProcess +from lava.proc.io.sink import RingBuffer as ReceiveProcess +from lava.magma.core.model.py.model import PyLoihiProcessModel + + +class TestRunConfig(RunConfig): + """Run configuration selects appropriate ProcessModel based on tag + """ + def __init__(self, select_tag: str = 'fixed_pt'): + super(TestRunConfig, self).__init__(custom_sync_domains=None) + self.select_tag = select_tag + + def select( + self, _, proc_models: List[PyLoihiProcessModel] + ) -> PyLoihiProcessModel: + for pm in proc_models: + if self.select_tag in pm.tags: + return pm + raise AssertionError('No legal ProcessModel found.') + + +if __name__ == '__main__': + num_steps = 10 + shape = (64, 32, 16) + input = np.random.randint(256, size=shape + (num_steps,)) + input -= 128 + + source = SendProcess(data=input) + sink = ReceiveProcess(shape=(np.prod(shape), ), buffer=num_steps) + source.out_ports.s_out.flatten().connect(sink.in_ports.a_in) + + run_condition = RunSteps(num_steps=num_steps) + run_config = TestRunConfig(select_tag='floating_pt') + sink.run(condition=run_condition, run_cfg=run_config) + output = sink.data.get() + sink.stop() + + expected = input.reshape([-1, num_steps]) + print(np.all(output == expected)) \ No newline at end of file From 917a497b6e1b129fbb73a0dba633eb061d353fc4 Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Fri, 4 Feb 2022 22:17:00 +0100 Subject: [PATCH 05/21] User-level API for TransposePort with unit tests Signed-off-by: Mathis Richter --- .../magma/core/process/ports/exceptions.py | 27 +++++-- src/lava/magma/core/process/ports/ports.py | 47 +++++++---- .../{test_permute.py => test_transpose.py} | 5 +- tests/lava/magma/core/process/test_ports.py | 79 ++++++++++++++++--- 4 files changed, 122 insertions(+), 36 deletions(-) rename tests/lava/magma/core/process/ports/{test_permute.py => test_transpose.py} (95%) diff --git a/src/lava/magma/core/process/ports/exceptions.py b/src/lava/magma/core/process/ports/exceptions.py index 60bb5b943..b93483a51 100644 --- a/src/lava/magma/core/process/ports/exceptions.py +++ b/src/lava/magma/core/process/ports/exceptions.py @@ -34,16 +34,33 @@ def __init__(self, shapes, axis): super().__init__(self, msg) -class PermuteError(Exception): - """Raised when permute order is incompatible with old shape dimension.""" +class TransposeShapeError(Exception): + """Raised when transpose axes is incompatible with old shape dimension.""" def __init__( - self, old_shape: ty.Tuple, order: ty.Union[ty.Tuple, ty.List] + self, old_shape: ty.Tuple, axes: ty.Union[ty.Tuple, ty.List] ) -> None: msg = ( - "Cannot permute 'old_shape'={} with permutation 'order={}. " + "Cannot transpose 'old_shape'={} with permutation 'axes={}. " "Total number of dimensions must not change during " - "reshaping.".format(old_shape, order) + "reshaping.".format(old_shape, axes) + ) + super().__init__(msg) + + +class TransposeIndexError(Exception): + """Raised when indices in transpose axes are out of bounds for the old + shape dimension.""" + + def __init__( + self, + old_shape: ty.Tuple, + axes: ty.Union[ty.Tuple, ty.List], + wrong_index + ) -> None: + msg = ( + f"Cannot transpose 'old_shape'={old_shape} with permutation" + f"'axes'={axes}. The index {wrong_index} is out of bounds." ) super().__init__(msg) diff --git a/src/lava/magma/core/process/ports/ports.py b/src/lava/magma/core/process/ports/ports.py index 4a1914202..50fb8540f 100644 --- a/src/lava/magma/core/process/ports/ports.py +++ b/src/lava/magma/core/process/ports/ports.py @@ -204,27 +204,40 @@ def concat_with( self._validate_ports(ports, port_type, assert_same_shape=False) return ConcatPort(ports, axis) - def permute( + def transpose( self, - order: ty.Union[ty.Tuple, ty.List] - ) -> "PermutePort": - """Permutes the tensor dimensio of this port by deriving and returning - a new virtual PermutePort the new permuted dimension. This implies that - the resulting PermutePort can only be forward connected to another port. + axes: ty.Optional[ty.Union[ty.Tuple, ty.List]] = None + ) -> "TransposePort": + """Permutes the tensor dimension of this port by deriving and returning + a new virtual TransposePort the new permuted dimension. This implies + that the resulting TransposePort can only be forward connected to + another port. Parameters ---------- - :param order: Order of permutation. Number of total elements and number + :param axes: Order of permutation. Number of total elements and number of dimensions must not change. """ - if len(self.shape) != len(order): - raise pe.PermuteError(self.shape, order) - - permute_port = PermutePort(tuple([self.shape[i] for i in order])) + if axes is None: + axes = tuple(reversed(range(len(self.shape)))) + else: + if len(self.shape) != len(axes): + raise pe.TransposeShapeError(self.shape, axes) + + # Check that none of the given axes are out of bounds for the + # shape of the parent port. + for idx in axes: + # Compute the positive index irrespective of the sign of 'idx' + idx_positive = len(self.shape) + idx if idx < 0 else idx + # Make sure the positive index is not out of bounds + if idx_positive < 0 or idx_positive >= len(self.shape): + raise pe.TransposeIndexError(self.shape, axes, idx) + + transpose_port = TransposePort(tuple([self.shape[i] for i in axes])) self._connect_forward( - [permute_port], AbstractPort, assert_same_shape=False + [transpose_port], AbstractPort, assert_same_shape=False ) - return permute_port + return transpose_port class AbstractIOPort(AbstractPort): @@ -711,8 +724,8 @@ def connect(self, ports: ty.Union["AbstractPort", ty.List["AbstractPort"]]): # ToDo: TBD... -class PermutePort(AbstractVirtualPort, AbstractPort): - """A PermutePort is a virtual port that allows to permute the dimensions +class TransposePort(AbstractVirtualPort, AbstractPort): + """A TransposePort is a virtual port that allows to permute the dimensions of a port before connecting to another port. It is used by the compiler to map the indices of the underlying tensor-valued data array from the derived to the new shape. @@ -720,10 +733,10 @@ class PermutePort(AbstractVirtualPort, AbstractPort): Example: out_port = OutPort((2, 4, 3)) in_port = InPort((3, 2, 4)) - out_port.permute([3, 1, 2]).connect(in_port) + out_port.transpose([3, 1, 2]).connect(in_port) """ - def __init__(self, shape: ty.Tuple): + def __init__(self, shape: ty.Optional[ty.Tuple] = None): AbstractPort.__init__(self, shape) @property diff --git a/tests/lava/magma/core/process/ports/test_permute.py b/tests/lava/magma/core/process/ports/test_transpose.py similarity index 95% rename from tests/lava/magma/core/process/ports/test_permute.py rename to tests/lava/magma/core/process/ports/test_transpose.py index 5fc140b9b..9035386a6 100644 --- a/tests/lava/magma/core/process/ports/test_permute.py +++ b/tests/lava/magma/core/process/ports/test_transpose.py @@ -1,6 +1,7 @@ # Copyright (C) 2021 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # See: https://spdx.org/licenses/ + from typing import List import unittest import numpy as np @@ -31,7 +32,7 @@ def select( class TestSendReceive(unittest.TestCase): - """Tests for all SendProces and ReceiveProcess.""" + """Tests for all SendProcess and ReceiveProcess.""" def test_permute(self) -> None: """Test whatever is being sent form source is received at sink.""" @@ -43,7 +44,7 @@ def test_permute(self) -> None: source = SendProcess(data=input) sink = ReceiveProcess(shape=shape[::-1], buffer=num_steps) - source.out_ports.s_out.permute([2, 1, 0]).connect(sink.in_ports.a_in) + source.out_ports.s_out.transpose([2, 1, 0]).connect(sink.in_ports.a_in) run_condition = RunSteps(num_steps=num_steps) run_config = TestRunConfig(select_tag='floating_pt') diff --git a/tests/lava/magma/core/process/test_ports.py b/tests/lava/magma/core/process/test_ports.py index 79a982ffe..fff104c06 100644 --- a/tests/lava/magma/core/process/test_ports.py +++ b/tests/lava/magma/core/process/test_ports.py @@ -9,11 +9,14 @@ RefPort, VarPort, ConcatPort, + TransposePort, ) from lava.magma.core.process.ports.exceptions import ( ReshapeError, DuplicateConnectionError, ConcatShapeError, + TransposeShapeError, + TransposeIndexError, VarNotSharableError, ) from lava.magma.core.process.variable import Var @@ -410,30 +413,33 @@ def test_connect_VarPort_to_InPort_OutPort_RefPort(self): class TestVirtualPorts(unittest.TestCase): """Contains unit tests around virtual ports. Virtual ports are derived - ports that are not directly created by developer as part of process + ports that are not directly created by the developer as part of process definition but which serve to somehow transform the properties of a - deverloper-defined port.""" + developer-defined port.""" def test_reshape(self): """Checks reshaping of a port.""" # Create some ports op = OutPort((1, 2, 3)) - ip1 = InPort((3, 2, 1)) - ip2 = InPort((3, 2, 10)) + ip = InPort((3, 2, 1)) # Using reshape(..), ports with different shape can be connected as # long as total number of elements does not change - op.reshape((3, 2, 1)).connect(ip1) + op.reshape((3, 2, 1)).connect(ip) # We can still find destination and source connection even with # virtual ports in the chain - self.assertEqual(op.get_dst_ports(), [ip1]) - self.assertEqual(ip1.get_src_ports(), [op]) + self.assertEqual(op.get_dst_ports(), [ip]) + self.assertEqual(ip.get_src_ports(), [op]) + + def test_reshape_with_wrong_number_of_elements_raises_exception(self): + """Checks whether an exception is raised when the number of elements + in the specified shape is different from the number of elements in + the source shape.""" - # However, ports with a different number of elements cannot be connected with self.assertRaises(ReshapeError): - op.reshape((3, 2, 10)).connect(ip2) + OutPort((1, 2, 3)).reshape((1, 2, 2)) def test_concat(self): """Checks concatenation of ports.""" @@ -469,7 +475,7 @@ def test_concat(self): # (2, 3, 1) + (2, 3, 1) concatenated along axis 1 results in (2, 6, 1) self.assertEqual(ip2.in_connections[0].shape, (2, 6, 1)) - def test_concat_with_incompatible_ports(self): + def test_concat_with_incompatible_shapes_raises_excpetion(self): """Checks that incompatible ports cannot be concatenated.""" # Create ports with incompatible shapes @@ -481,11 +487,60 @@ def test_concat_with_incompatible_ports(self): with self.assertRaises(ConcatShapeError): op1.concat_with(op2, axis=0) - # Create another port with incompatible type + def test_concat_with_incompatible_type_raises_exception(self): + """Checks that incompatible port types raise an exception.""" + + op = OutPort((2, 3, 1)) ip = InPort((2, 3, 1)) # This will fail because concatenated ports must be of same type with self.assertRaises(AssertionError): - op1.concat_with(ip, axis=0) + op.concat_with(ip, axis=0) + + def test_transpose(self): + """Checks transposing of ports.""" + + op = OutPort((1, 2, 3)) + ip = InPort((2, 1, 3)) + + tp = op.transpose(axes=(1, 0, 2)) + # The return value is a virtual TransposePort ... + self.assertIsInstance(tp, TransposePort) + # ... which needs to have the same dimensions ... + self.assertEqual(tp.shape, (2, 1, 3)) + # ... as the port we want to connect to. + tp.connect(ip) + # Finally, the virtual TransposePort is the input connection of the ip + self.assertEqual(tp, ip.in_connections[0]) + + # Again, we can still find destination and source ports through a + # chain of ports containing virtual ports + self.assertEqual(op.get_dst_ports(), [ip]) + self.assertEqual(ip.get_src_ports(), [op]) + + def test_transpose_without_specified_axes(self): + """Checks whether transpose reverses the shape-elements when no + 'axes' argument is given.""" + + op = OutPort((1, 2, 3)) + tp = op.transpose() + self.assertEqual(tp.shape, (3, 2, 1)) + + def test_transpose_incompatible_axes_length_raises_exception(self): + """Checks whether an exception is raised when the number of elements + in the specified 'axes' argument differs from the number of elements + of the parent port.""" + + op = OutPort((1, 2, 3)) + with self.assertRaises(TransposeShapeError): + op.transpose(axes=(0, 0, 1, 2)) + + def test_transpose_incompatible_axes_indices_raises_exception(self): + """Checks whether an exception is raised when the indices specified + in the 'axes' argument are out of bounds for the parent port.""" + + op = OutPort((1, 2, 3)) + with self.assertRaises(TransposeIndexError): + op.transpose(axes=(0, 1, 3)) if __name__ == "__main__": From 0f337744dc47dc6a928aae242d08a67b455da2d6 Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Fri, 4 Feb 2022 22:24:23 +0100 Subject: [PATCH 06/21] Fixed typo Signed-off-by: Mathis Richter --- tests/lava/magma/core/process/test_ports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lava/magma/core/process/test_ports.py b/tests/lava/magma/core/process/test_ports.py index fff104c06..00211c09e 100644 --- a/tests/lava/magma/core/process/test_ports.py +++ b/tests/lava/magma/core/process/test_ports.py @@ -475,7 +475,7 @@ def test_concat(self): # (2, 3, 1) + (2, 3, 1) concatenated along axis 1 results in (2, 6, 1) self.assertEqual(ip2.in_connections[0].shape, (2, 6, 1)) - def test_concat_with_incompatible_shapes_raises_excpetion(self): + def test_concat_with_incompatible_shapes_raises_exception(self): """Checks that incompatible ports cannot be concatenated.""" # Create ports with incompatible shapes From 1537c4118a4b62389a6acece21b663aa2953a76d Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Fri, 4 Feb 2022 22:50:02 +0100 Subject: [PATCH 07/21] Unit tests for flatten() and concat_with() Signed-off-by: Mathis Richter --- .../magma/core/process/ports/exceptions.py | 11 ++++++++ src/lava/magma/core/process/ports/ports.py | 3 ++ tests/lava/magma/core/process/test_ports.py | 28 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/src/lava/magma/core/process/ports/exceptions.py b/src/lava/magma/core/process/ports/exceptions.py index b93483a51..ed65ad99d 100644 --- a/src/lava/magma/core/process/ports/exceptions.py +++ b/src/lava/magma/core/process/ports/exceptions.py @@ -34,6 +34,17 @@ def __init__(self, shapes, axis): super().__init__(self, msg) +class ConcatIndexError(Exception): + """Raised when the axis over which ports should be concatenated is out of + bounds.""" + + def __init__(self, shape: ty.Tuple[int], axis: int): + msg = ( + "Axis {} is out of bounds for given shape {}.".format(axis, shape) + ) + super().__init__(self, msg) + + class TransposeShapeError(Exception): """Raised when transpose axes is incompatible with old shape dimension.""" diff --git a/src/lava/magma/core/process/ports/ports.py b/src/lava/magma/core/process/ports/ports.py index 50fb8540f..abec55cf2 100644 --- a/src/lava/magma/core/process/ports/ports.py +++ b/src/lava/magma/core/process/ports/ports.py @@ -678,6 +678,9 @@ def _get_new_shape(ports: ty.List[AbstractPort], axis): shapes_ex_axis = [] shapes_incompatible = False for shape in concat_shapes: + if axis >= len(shape): + raise pe.ConcatIndexError(shape, axis) + # Compute total size along concatenation axis total_size += shape[axis] # Extract shape dimensions other than concatenation axis diff --git a/tests/lava/magma/core/process/test_ports.py b/tests/lava/magma/core/process/test_ports.py index 00211c09e..11c5a2e7e 100644 --- a/tests/lava/magma/core/process/test_ports.py +++ b/tests/lava/magma/core/process/test_ports.py @@ -15,6 +15,7 @@ ReshapeError, DuplicateConnectionError, ConcatShapeError, + ConcatIndexError, TransposeShapeError, TransposeIndexError, VarNotSharableError, @@ -441,6 +442,24 @@ def test_reshape_with_wrong_number_of_elements_raises_exception(self): with self.assertRaises(ReshapeError): OutPort((1, 2, 3)).reshape((1, 2, 2)) + def test_flatten(self): + """Checks flattening of a port.""" + + op = OutPort((1, 2, 3)) + ip = InPort((6,)) + + # Flatten the shape of the port. + fp = op.flatten() + self.assertEqual(fp.shape, (6,)) + + # This enables connecting to an input port with a flattened shape. + fp.connect(ip) + + # We can still find destination and source connection even with + # virtual ports in the chain + self.assertEqual(op.get_dst_ports(), [ip]) + self.assertEqual(ip.get_src_ports(), [op]) + def test_concat(self): """Checks concatenation of ports.""" @@ -496,6 +515,15 @@ def test_concat_with_incompatible_type_raises_exception(self): with self.assertRaises(AssertionError): op.concat_with(ip, axis=0) + def test_concat_with_axis_out_of_bounds_raises_exception(self): + """Checks whether an exception is raised when the specified axis is + out of bounds.""" + + op1 = OutPort((2, 3, 1)) + op2 = OutPort((2, 3, 1)) + with self.assertRaises(ConcatIndexError): + op1.concat_with(op2, axis=3) + def test_transpose(self): """Checks transposing of ports.""" From 938b7db070d02ecbd82061b76df45a242503b521 Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Fri, 11 Feb 2022 11:35:23 +0100 Subject: [PATCH 08/21] Unit tests for virtual ports in Processes that are executed (wip) Signed-off-by: Mathis Richter --- .../magma/core/process/ports/test_flatten.py | 48 -- .../core/process/{ => ports}/test_ports.py | 3 +- .../core/process/ports/test_transpose.py | 89 --- .../ports/test_virtual_ports_in_process.py | 594 ++++++++++++++++++ .../process/test_virtual_ports_in_process.py | 123 ---- 5 files changed, 596 insertions(+), 261 deletions(-) delete mode 100644 tests/lava/magma/core/process/ports/test_flatten.py rename tests/lava/magma/core/process/{ => ports}/test_ports.py (99%) delete mode 100644 tests/lava/magma/core/process/ports/test_transpose.py create mode 100644 tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py delete mode 100644 tests/lava/magma/core/process/test_virtual_ports_in_process.py diff --git a/tests/lava/magma/core/process/ports/test_flatten.py b/tests/lava/magma/core/process/ports/test_flatten.py deleted file mode 100644 index daa6f2160..000000000 --- a/tests/lava/magma/core/process/ports/test_flatten.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# SPDX-License-Identifier: BSD-3-Clause -# See: https://spdx.org/licenses/ - -from typing import List -import numpy as np - -from lava.magma.core.run_configs import RunConfig -from lava.magma.core.run_conditions import RunSteps -from lava.proc.io.source import RingBuffer as SendProcess -from lava.proc.io.sink import RingBuffer as ReceiveProcess -from lava.magma.core.model.py.model import PyLoihiProcessModel - - -class TestRunConfig(RunConfig): - """Run configuration selects appropriate ProcessModel based on tag - """ - def __init__(self, select_tag: str = 'fixed_pt'): - super(TestRunConfig, self).__init__(custom_sync_domains=None) - self.select_tag = select_tag - - def select( - self, _, proc_models: List[PyLoihiProcessModel] - ) -> PyLoihiProcessModel: - for pm in proc_models: - if self.select_tag in pm.tags: - return pm - raise AssertionError('No legal ProcessModel found.') - - -if __name__ == '__main__': - num_steps = 10 - shape = (64, 32, 16) - input = np.random.randint(256, size=shape + (num_steps,)) - input -= 128 - - source = SendProcess(data=input) - sink = ReceiveProcess(shape=(np.prod(shape), ), buffer=num_steps) - source.out_ports.s_out.flatten().connect(sink.in_ports.a_in) - - run_condition = RunSteps(num_steps=num_steps) - run_config = TestRunConfig(select_tag='floating_pt') - sink.run(condition=run_condition, run_cfg=run_config) - output = sink.data.get() - sink.stop() - - expected = input.reshape([-1, num_steps]) - print(np.all(output == expected)) \ No newline at end of file diff --git a/tests/lava/magma/core/process/test_ports.py b/tests/lava/magma/core/process/ports/test_ports.py similarity index 99% rename from tests/lava/magma/core/process/test_ports.py rename to tests/lava/magma/core/process/ports/test_ports.py index 11c5a2e7e..0e6fe26c4 100644 --- a/tests/lava/magma/core/process/test_ports.py +++ b/tests/lava/magma/core/process/ports/test_ports.py @@ -1,8 +1,10 @@ # Copyright (C) 2021 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # See: https://spdx.org/licenses/ + import unittest 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, @@ -20,7 +22,6 @@ TransposeIndexError, VarNotSharableError, ) -from lava.magma.core.process.variable import Var class TestPortInitialization(unittest.TestCase): diff --git a/tests/lava/magma/core/process/ports/test_transpose.py b/tests/lava/magma/core/process/ports/test_transpose.py deleted file mode 100644 index 9035386a6..000000000 --- a/tests/lava/magma/core/process/ports/test_transpose.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# SPDX-License-Identifier: BSD-3-Clause -# See: https://spdx.org/licenses/ - -from typing import List -import unittest -import numpy as np - -from lava.magma.core.run_configs import RunConfig -from lava.magma.core.run_conditions import RunSteps -from lava.proc.io.source import RingBuffer as SendProcess -from lava.proc.io.sink import RingBuffer as ReceiveProcess -from lava.magma.core.model.py.model import PyLoihiProcessModel - -np.random.seed(7739) - - -class TestRunConfig(RunConfig): - """Run configuration selects appropriate ProcessModel based on tag - """ - def __init__(self, select_tag: str = 'fixed_pt'): - super(TestRunConfig, self).__init__(custom_sync_domains=None) - self.select_tag = select_tag - - def select( - self, _, proc_models: List[PyLoihiProcessModel] - ) -> PyLoihiProcessModel: - for pm in proc_models: - if self.select_tag in pm.tags: - return pm - raise AssertionError('No legal ProcessModel found.') - - -class TestSendReceive(unittest.TestCase): - """Tests for all SendProcess and ReceiveProcess.""" - - def test_permute(self) -> None: - """Test whatever is being sent form source is received at sink.""" - num_steps = 10 - shape = (64, 32, 16) - input = np.random.randint(256, size=shape + (num_steps,)) - input -= 128 - # input = 0.5 * input - - source = SendProcess(data=input) - sink = ReceiveProcess(shape=shape[::-1], buffer=num_steps) - source.out_ports.s_out.transpose([2, 1, 0]).connect(sink.in_ports.a_in) - - run_condition = RunSteps(num_steps=num_steps) - run_config = TestRunConfig(select_tag='floating_pt') - sink.run(condition=run_condition, run_cfg=run_config) - output = sink.data.get() - sink.stop() - - expected = input.permute([2, 1, 0]) - self.assertTrue( - np.all(output == expected), - f'Input and Ouptut do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' - ) - - -if __name__ == '__main__': - num_steps = 10 - shape = (64, 32, 16) - input = np.random.randint(256, size=shape + (num_steps,)) - input -= 128 - # input = 0.5 * input - - source = SendProcess(data=input) - # sink = ReceiveProcess(shape=shape, buffer=num_steps) - # source.out_ports.s_out.connect(sink.in_ports.a_in) - - # sink = ReceiveProcess(shape=shape[::-1], buffer=num_steps) - # source.out_ports.s_out.permute([2, 1, 0]).connect(sink.in_ports.a_in) - - sink = ReceiveProcess(shape=(np.prod(shape), ), buffer=num_steps) - source.out_ports.s_out.flatten().connect(sink.in_ports.a_in) - - run_condition = RunSteps(num_steps=num_steps) - run_config = TestRunConfig(select_tag='floating_pt') - sink.run(condition=run_condition, run_cfg=run_config) - output = sink.data.get() - sink.stop() - - # expected = input.transpose([2, 1, 0, 3]) - expected = input.reshape([-1, num_steps]) - print(np.all(output == expected)) diff --git a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py new file mode 100644 index 000000000..d5bfba7b1 --- /dev/null +++ b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py @@ -0,0 +1,594 @@ +# Copyright (C) 2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# See: https://spdx.org/licenses/ + +import typing as ty +import unittest +import numpy as np + +from lava.magma.core.decorator import requires, tag, implements +from lava.magma.core.model.py.model import PyLoihiProcessModel +from lava.magma.core.model.py.type import LavaPyType +from lava.magma.core.process.variable import Var +from lava.magma.core.process.process import AbstractProcess +from lava.magma.core.resources import CPU +from lava.magma.core.run_conditions import RunSteps +from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol +from lava.magma.core.run_configs import Loihi1SimCfg +from lava.magma.core.model.py.ports import ( + PyInPort, + PyOutPort, + PyRefPort, + PyVarPort +) +from lava.magma.core.process.ports.ports import ( + InPort, + OutPort, + RefPort, + VarPort +) + + +np.random.seed(7739) + + +class TestTransposePort(unittest.TestCase): + """Tests virtual TransposePorts on Processes that are executed.""" + + def setUp(self) -> None: + self.num_steps = 1 + self.axes = (2, 0, 1) + self.axes_reverse = list(self.axes) + for idx, ax in enumerate(self.axes): + self.axes_reverse[ax] = idx + self.axes_reverse = tuple(self.axes_reverse) + self.shape = (4, 3, 2) + self.shape_transposed = tuple(self.shape[i] for i in self.axes) + self.shape_transposed_reverse = \ + tuple(self.shape[i] for i in self.axes_reverse) + self.input_data = np.random.randint(256, size=self.shape) + + def test_transpose_outport_to_inport(self) -> None: + """Tests a virtual TransposePort between an OutPort and an InPort.""" + + source = OutPortProcess(data=self.input_data) + sink = InPortProcess(shape=self.shape_transposed) + + source.out_port.transpose(axes=self.axes).connect(sink.in_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.transpose(self.axes) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_transpose_inport_to_inport(self) -> None: + """Tests a virtual TransposePort between an InPort and another InPort. + In a real implementation, the source InPort would be in a + hierarchical Process and the sink InPort would be in a SubProcess of + that hierarchical Process.""" + + out_port_process = OutPortProcess(data=self.input_data) + source = InPortProcess(shape=self.shape) + sink = InPortProcess(shape=self.shape_transposed) + + out_port_process.out_port.connect(source.in_port) + source.in_port.transpose(axes=self.axes).connect(sink.in_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.transpose(self.axes) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_transpose_refport_write_to_varport(self) -> None: + """Tests a virtual TransposePort between a RefPort and a VarPort, + where the RefPort writes to the VarPort.""" + + source = RefPortWriteProcess(data=self.input_data) + sink = VarPortProcess(data=np.zeros(self.shape_transposed)) + source.ref_port.transpose(axes=self.axes).connect(sink.var_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.transpose(self.axes) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_transpose_refport_read_from_varport(self) -> None: + """Tests a virtual TransposePort between a RefPort and a VarPort, + where the RefPort reads from the VarPort.""" + + source = VarPortProcess(data=self.input_data) + sink = RefPortReadProcess(data=np.zeros(self.shape_transposed_reverse)) + sink.ref_port.transpose(axes=self.axes).connect(source.var_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.transpose(self.axes_reverse) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + @unittest.skip("RefPort to RefPort not yet implemented") + def test_transpose_refport_write_to_refport(self) -> None: + """Tests a virtual TransposePort between a RefPort and another + RefPort, where the first RefPort writes to the second. In a real + implementation, the source RefPort would be in a + hierarchical Process and the sink RefPort would be in a SubProcess of + that hierarchical Process.""" + + source = RefPortWriteProcess(data=self.input_data) + sink = RefPortReadProcess(data=np.zeros(self.shape_transposed)) + var_port_process = VarPortProcess(data=np.zeros(self.shape_transposed)) + + source.ref_port.transpose(axes=self.axes).connect(sink.ref_port) + sink.ref_port.connect(var_port_process.var_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = var_port_process.data.get() + sink.stop() + + expected = self.input_data.transpose(self.axes) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + @unittest.skip("RefPort to RefPort not yet implemented") + def test_transpose_refport_read_from_refport(self) -> None: + """Tests a virtual TransposePort between a RefPort and another + RefPort, where the first RefPort reads from the second. In a real + implementation, the source RefPort would be in a + hierarchical Process and the sink RefPort would be in a SubProcess of + that hierarchical Process.""" + + source = RefPortReadProcess( + data=np.zeros(self.shape_transposed_reverse) + ) + sink = RefPortWriteProcess(data=self.input_data) + + sink.ref_port.transpose(axes=self.axes).connect(source.ref_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.transpose(self.axes_reverse) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + @unittest.skip("VarPort to VarPort not yet implemented") + def test_transpose_varport_write_to_varport(self) -> None: + """Tests a virtual TransposePort between a VarPort and another + VarPort, where the first VarPort writes to the second. In a + real implementation, the source VarPort would be in a + hierarchical Process and the sink VarPort would be in a SubProcess of + that hierarchical Process.""" + + source = VarPortProcess(data=self.input_data) + sink = VarPortProcess(data=np.zeros(self.shape_transposed)) + + source.var_port.transpose(axes=self.axes).connect(sink.var_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.transpose(self.axes) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + @unittest.skip("VarPort to VarPort not yet implemented") + def test_transpose_varport_read_from_varport(self) -> None: + """Tests a virtual TransposePort between a VarPort and another + VarPort, where the first VarPort reads from the second. In a real + implementation, the source VarPort would be in a + hierarchical Process and the sink VarPort would be in a SubProcess of + that hierarchical Process.""" + + sink = VarPortProcess(data=np.zeros(self.shape_transposed_reverse)) + source = VarPortProcess(data=self.input_data) + + sink.var_port.transpose(axes=self.axes).connect(source.var_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.transpose(self.axes_reverse) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + +class TestReshapePort(unittest.TestCase): + """Tests virtual ReshapePorts on Processes that are executed.""" + + def setUp(self) -> None: + self.num_steps = 1 + self.shape = (4, 3, 2) + self.shape_reshaped = (12, 2) + self.input_data = np.random.randint(256, size=self.shape) + + def test_reshape_outport_to_inport(self) -> None: + """Tests a virtual ReshapePort between an OutPort and an InPort.""" + + source = OutPortProcess(data=self.input_data) + sink = InPortProcess(shape=self.shape_reshaped) + + source.out_port.reshape(new_shape=self.shape_reshaped).connect( + sink.in_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.reshape(self.shape_reshaped) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_reshape_inport_to_inport(self) -> None: + """Tests a virtual ReshapePort between an InPort and another InPort. + In a real implementation, the source InPort would be in a + hierarchical Process and the sink InPort would be in a SubProcess of + that hierarchical Process.""" + + out_port_process = OutPortProcess(data=self.input_data) + source = InPortProcess(shape=self.shape) + sink = InPortProcess(shape=self.shape_reshaped) + + out_port_process.out_port.connect(source.in_port) + source.in_port.reshape(new_shape=self.shape_reshaped).connect( + sink.in_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.reshape(self.shape_reshaped) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_reshape_refport_write_to_varport(self) -> None: + """Tests a virtual ReshapePort between a RefPort and a VarPort, + where the RefPort writes to the VarPort.""" + + source = RefPortWriteProcess(data=self.input_data) + sink = VarPortProcess(data=np.zeros(self.shape_reshaped)) + source.ref_port.reshape(new_shape=self.shape_reshaped).connect( + sink.var_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.reshape(self.shape_reshaped) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_reshape_refport_read_from_varport(self) -> None: + """Tests a virtual ReshapePort between a RefPort and a VarPort, + where the RefPort reads from the VarPort.""" + + source = VarPortProcess(data=self.input_data) + sink = RefPortReadProcess(data=np.zeros(self.shape_reshaped)) + sink.ref_port.reshape(new_shape=self.shape_reshaped).connect( + source.var_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.reshape(self.shape_reshaped) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + +class TestFlattenPort(unittest.TestCase): + """Tests virtual FlattenPorts on Processes that are executed.""" + + def setUp(self) -> None: + self.num_steps = 1 + self.shape = (4, 3, 2) + self.shape_reshaped = (24,) + self.input_data = np.random.randint(256, size=self.shape) + + def test_flatten_outport_to_inport(self) -> None: + """Tests a virtual FlattenPort between an OutPort and an InPort.""" + + source = OutPortProcess(data=self.input_data) + sink = InPortProcess(shape=self.shape_reshaped) + + source.out_port.flatten().connect(sink.in_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.ravel() + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_flatten_inport_to_inport(self) -> None: + """Tests a virtual FlattenPort between an InPort and another InPort. + In a real implementation, the source InPort would be in a + hierarchical Process and the sink InPort would be in a SubProcess of + that hierarchical Process.""" + + out_port_process = OutPortProcess(data=self.input_data) + source = InPortProcess(shape=self.shape) + sink = InPortProcess(shape=self.shape_reshaped) + + out_port_process.out_port.connect(source.in_port) + source.in_port.flatten().connect(sink.in_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.ravel() + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_flatten_refport_write_to_varport(self) -> None: + """Tests a virtual FlattenPort between a RefPort and a VarPort, + where the RefPort writes to the VarPort.""" + + source = RefPortWriteProcess(data=self.input_data) + sink = VarPortProcess(data=np.zeros(self.shape_reshaped)) + source.ref_port.flatten().connect(sink.var_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.ravel() + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_flatten_refport_read_from_varport(self) -> None: + """Tests a virtual FlattenPort between a RefPort and a VarPort, + where the RefPort reads from the VarPort.""" + + source = VarPortProcess(data=self.input_data) + sink = RefPortReadProcess(data=np.zeros(self.shape_reshaped)) + sink.ref_port.flatten().connect(source.var_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.ravel() + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + +class TestConcatPort(unittest.TestCase): + """Tests virtual ConcatPorts on Processes that are executed.""" + + def setUp(self) -> None: + self.num_steps = 1 + self.shape = (3, 1, 4) + self.shape_concat = (3, 3, 4) + self.input_data = np.random.randint(256, size=self.shape) + + def test_concat_outport_to_inport(self) -> None: + """Tests a virtual ConcatPort between an OutPort and an InPort.""" + + source_1 = OutPortProcess(data=self.input_data) + source_2 = OutPortProcess(data=self.input_data) + source_3 = OutPortProcess(data=self.input_data) + sink = InPortProcess(shape=self.shape_concat) + + source_1.out_port.concat_with([ + source_2.out_port, + source_3.out_port], axis=1).connect(sink.in_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = np.concatenate([self.input_data] * 3, axis=1) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + +# minimal process with an OutPort +class OutPortProcess(AbstractProcess): + def __init__(self, data: np.ndarray) -> None: + super().__init__(data=data) + self.data = Var(shape=data.shape, init=data) + self.out_port = OutPort(shape=data.shape) + + +# minimal process with an InPort +class InPortProcess(AbstractProcess): + def __init__(self, shape: ty.Tuple[int, ...]) -> None: + super().__init__(shape=shape) + self.data = Var(shape=shape, init=np.zeros(shape)) + self.in_port = InPort(shape=shape) + + +# A minimal process with a RefPort that writes +class RefPortWriteProcess(AbstractProcess): + def __init__(self, data: np.ndarray) -> None: + super().__init__(data=data) + self.data = Var(shape=data.shape, init=data) + self.ref_port = RefPort(shape=data.shape) + + +# A minimal process with a RefPort that reads +class RefPortReadProcess(AbstractProcess): + def __init__(self, data: np.ndarray) -> None: + super().__init__(data=data) + self.data = Var(shape=data.shape, init=data) + self.ref_port = RefPort(shape=data.shape) + + +# A minimal process with a Var and a VarPort +class VarPortProcess(AbstractProcess): + def __init__(self, data: np.ndarray) -> None: + super().__init__(data=data) + self.data = Var(shape=data.shape, init=data) + self.var_port = VarPort(self.data) + + +# A minimal PyProcModel implementing OutPortProcess +@implements(proc=OutPortProcess, protocol=LoihiProtocol) +@requires(CPU) +@tag('floating_pt') +class PyOutPortProcessModelFloat(PyLoihiProcessModel): + out_port: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, np.int32) + data: np.ndarray = LavaPyType(np.ndarray, np.int32) + + def run_spk(self): + self.out_port.send(self.data) + print("Sent output data of OutPortProcess: ", str(self.data)) + + +# A minimal PyProcModel implementing InPortProcess +@implements(proc=InPortProcess, protocol=LoihiProtocol) +@requires(CPU) +@tag('floating_pt') +class PyInPortProcessModelFloat(PyLoihiProcessModel): + in_port: PyInPort = LavaPyType(PyInPort.VEC_DENSE, np.int32) + data: np.ndarray = LavaPyType(np.ndarray, np.int32) + + def run_spk(self): + self.data[:] = self.in_port.recv() + print("Received input data for InPortProcess: ", str(self.data)) + + +# A minimal PyProcModel implementing RefPortWriteProcess +@implements(proc=RefPortWriteProcess, protocol=LoihiProtocol) +@requires(CPU) +@tag('floating_pt') +class PyRefPortWriteProcessModelFloat(PyLoihiProcessModel): + ref_port: PyRefPort = LavaPyType(PyRefPort.VEC_DENSE, np.int32) + data: np.ndarray = LavaPyType(np.ndarray, np.int32) + + def pre_guard(self): + return True + + def run_pre_mgmt(self): + self.ref_port.write(self.data) + print("Sent output data of RefPortWriteProcess: ", str(self.data)) + + +# A minimal PyProcModel implementing RefPortReadProcess +@implements(proc=RefPortReadProcess, protocol=LoihiProtocol) +@requires(CPU) +@tag('floating_pt') +class PyRefPortReadProcessModelFloat(PyLoihiProcessModel): + ref_port: PyRefPort = LavaPyType(PyRefPort.VEC_DENSE, np.int32) + data: np.ndarray = LavaPyType(np.ndarray, np.int32) + + def pre_guard(self): + return True + + def run_pre_mgmt(self): + self.data = self.ref_port.read() + print("Received input data for RefPortReadProcess: ", str(self.data)) + + +# A minimal PyProcModel implementing VarPortProcess +@implements(proc=VarPortProcess, protocol=LoihiProtocol) +@requires(CPU) +@tag('floating_pt') +class PyVarPortProcessModelFloat(PyLoihiProcessModel): + var_port: PyInPort = LavaPyType(PyVarPort.VEC_DENSE, np.int32) + data: np.ndarray = LavaPyType(np.ndarray, np.int32) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/lava/magma/core/process/test_virtual_ports_in_process.py b/tests/lava/magma/core/process/test_virtual_ports_in_process.py deleted file mode 100644 index 84c086bb2..000000000 --- a/tests/lava/magma/core/process/test_virtual_ports_in_process.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# SPDX-License-Identifier: BSD-3-Clause -import unittest -import numpy as np - -from lava.magma.core.decorator import implements, requires -from lava.magma.core.model.py.model import AbstractPyProcessModel -from lava.magma.core.model.py.ports import PyInPort, PyOutPort -from lava.magma.core.model.py.type import LavaPyType -from lava.magma.core.process.ports.ports import ( - InPort, - OutPort, -) -from lava.magma.core.process.process import AbstractProcess -from lava.magma.core.resources import CPU -from lava.magma.core.run_conditions import RunSteps -from lava.magma.core.run_configs import RunConfig - - -class TestVirtualPorts(unittest.TestCase): - """Contains unit tests around virtual ports created as part of a Process.""" - - def setUp(self) -> None: - # minimal process with an OutPort - pass - - @unittest.skip("skip while solved") - def test_multi_inports(self): - sender = P1() - recv1 = P2() - recv2 = P2() - recv3 = P2() - - # An OutPort can connect to multiple InPorts - # Either at once... - sender.out.connect([recv1.inp, recv2.inp, recv3.inp]) - - sender = P1() - recv1 = P2() - recv2 = P2() - recv3 = P2() - - # ... or consecutively - sender.out.connect(recv1.inp) - sender.out.connect(recv2.inp) - sender.out.connect(recv3.inp) - sender.run(RunSteps(num_steps=2), MyRunCfg()) - - @unittest.skip("skip while solved") - def test_reshape(self): - """Checks reshaping of a port.""" - sender = P1(shape=(1, 6)) - recv = P2(shape=(2, 3)) - - # Using reshape(..), ports with different shape can be connected as - # long as total number of elements does not change - sender.out.reshape((2, 3)).connect(recv.inp) - sender.run(RunSteps(num_steps=2), MyRunCfg()) - - @unittest.skip("skip while solved") - def test_concat(self): - """Checks concatenation of ports.""" - sender1 = P1(shape=(1, 2)) - sender2 = P1(shape=(1, 2)) - sender3 = P1(shape=(1, 2)) - recv = P2(shape=(3, 2)) - - # concat_with(..) concatenates calling port (sender1.out) with - # other ports (sender2.out, sender3.out) along given axis - cp = sender1.out.concat_with([sender2.out, sender3.out], axis=0) - - # The return value is a virtual ConcatPort which can be connected - # to the input port - cp.connect(recv.inp) - sender1.run(RunSteps(num_steps=2), MyRunCfg()) - - -# minimal process with an OutPort -class P1(AbstractProcess): - def __init__(self, **kwargs): - super().__init__(**kwargs) - shape = kwargs.get('shape', (3,)) - self.out = OutPort(shape=shape) - - -# minimal process with an InPort -class P2(AbstractProcess): - def __init__(self, **kwargs): - super().__init__(**kwargs) - shape = kwargs.get('shape', (3,)) - self.inp = InPort(shape=shape) - - -# A minimal PyProcModel implementing P1 -@implements(proc=P1) -@requires(CPU) -class PyProcModelA(AbstractPyProcessModel): - out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, int) - - def run(self): - data = np.asarray([1, 2, 3]) - self.out.send(data) - print("Sent output data of P1: ", str(data)) - - -# A minimal PyProcModel implementing P2 -@implements(proc=P2) -@requires(CPU) -class PyProcModelB(AbstractPyProcessModel): - inp: PyInPort = LavaPyType(PyInPort.VEC_DENSE, int) - - def run(self): - in_data = self.inp.recv() - print("Received input data for P2: ", str(in_data)) - - -class MyRunCfg(RunConfig): - def select(self, proc, proc_models): - return proc_models[0] - - -if __name__ == '__main__': - unittest.main() From d0eff5ad3b33fb8cf4c1e9e4bbfe6d0f26a7236e Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Thu, 17 Feb 2022 21:56:33 +0100 Subject: [PATCH 09/21] Preliminary implementation of virtual ports between OutPort and InPort (wip) Signed-off-by: Mathis Richter --- src/lava/magma/compiler/builder.py | 12 +- src/lava/magma/compiler/compiler.py | 22 ++- src/lava/magma/compiler/utils.py | 2 + src/lava/magma/core/model/py/ports.py | 30 +++- src/lava/magma/core/process/ports/ports.py | 144 +++++++----------- tests/lava/magma/core/model/py/test_ports.py | 2 +- .../ports/test_virtual_ports_in_process.py | 115 +++++++++++--- 7 files changed, 214 insertions(+), 113 deletions(-) diff --git a/src/lava/magma/compiler/builder.py b/src/lava/magma/compiler/builder.py index 1ae562041..05debb477 100644 --- a/src/lava/magma/compiler/builder.py +++ b/src/lava/magma/compiler/builder.py @@ -372,7 +372,17 @@ def build(self): csp_ports = self.csp_ports[name] if not isinstance(csp_ports, list): csp_ports = [csp_ports] - port = port_cls(csp_ports, pm, p.shape, lt.d_type) + + # TODO (MR): This is probably just a temporary hack until the + # interface of PyOutPorts has been adjusted. + if issubclass(port_cls, PyInPort): + port = port_cls(csp_ports, p.transform_funcs, pm, p.shape, + lt.d_type) + elif issubclass(port_cls, PyOutPort): + port = port_cls(csp_ports, pm, p.shape, lt.d_type) + else: + raise AssertionError("port_cls must be of type PyInPort or " + "PyOutPort") # Create dynamic PyPort attribute on ProcModel setattr(pm, name, port) diff --git a/src/lava/magma/compiler/compiler.py b/src/lava/magma/compiler/compiler.py index f6b958ed7..a8686f8d9 100644 --- a/src/lava/magma/compiler/compiler.py +++ b/src/lava/magma/compiler/compiler.py @@ -1,6 +1,7 @@ # Copyright (C) 2021 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # See: https://spdx.org/licenses/ + import importlib import importlib.util as import_utils import inspect @@ -34,7 +35,7 @@ from lava.magma.core.model.py.ports import RefVarTypeMapping from lava.magma.core.model.sub.model import AbstractSubProcessModel from lava.magma.core.process.ports.ports import AbstractPort, VarPort, \ - ImplicitVarPort + ImplicitVarPort, InPort from lava.magma.core.process.process import AbstractProcess from lava.magma.core.resources import CPU, NeuroCore from lava.magma.core.run_configs import RunConfig @@ -344,12 +345,25 @@ def _compile_proc_models( # and Ports v = [VarInitializer(v.name, v.shape, v.init, v.id) for v in p.vars] - ports = (list(p.in_ports) + list(p.out_ports)) - ports = [PortInitializer(pt.name, + + ports = [] + for pt in (list(p.in_ports) + list(p.out_ports)): + # For all InPorts that receive input from + # virtual ports... + transform_funcs = None + if isinstance(pt, InPort): + # ... extract a function pointer to the + # transformation function of each virtual port. + transform_funcs = \ + [vp.get_transform_func() + for vp in pt.get_incoming_virtual_ports()] + pi = PortInitializer(pt.name, pt.shape, self._get_port_dtype(pt, pm), pt.__class__.__name__, - pp_ch_size) for pt in ports] + pp_ch_size, + transform_funcs) + ports.append(pi) # Create RefPort (also use PortInitializers) ref_ports = list(p.ref_ports) ref_ports = [ diff --git a/src/lava/magma/compiler/utils.py b/src/lava/magma/compiler/utils.py index 14da90baf..27f4775fb 100644 --- a/src/lava/magma/compiler/utils.py +++ b/src/lava/magma/compiler/utils.py @@ -1,4 +1,5 @@ import typing as ty +import functools as ft from dataclasses import dataclass @@ -17,6 +18,7 @@ class PortInitializer: d_type: type port_type: str size: int + transform_funcs: ty.List[ft.partial] = None # check if can be a subclass of PortInitializer diff --git a/src/lava/magma/core/model/py/ports.py b/src/lava/magma/core/model/py/ports.py index e2c35bc9f..9871fa656 100644 --- a/src/lava/magma/core/model/py/ports.py +++ b/src/lava/magma/core/model/py/ports.py @@ -30,8 +30,13 @@ class PyInPort(AbstractPyPort): SCALAR_DENSE: ty.Type["PyInPortScalarDense"] = None SCALAR_SPARSE: ty.Type["PyInPortScalarSparse"] = None - def __init__(self, csp_recv_ports: ty.List[CspRecvPort], *args): + def __init__(self, + csp_recv_ports: ty.List[CspRecvPort], + transform_funcs: ty.Optional[ty.List[ft.partial]] = None, + *args): self._csp_recv_ports = csp_recv_ports + self._transform_funcs = transform_funcs + super().__init__(*args) @property @@ -61,18 +66,37 @@ def probe(self) -> bool: True, ) + def _transform(self, recv_data: np.array) -> np.array: + """Applies all transformation function pointers to the input data. + + Parameters + ---------- + recv_data : numpy.ndarray + data received on the port that shall be transformed + + Returns + ------- + recv_data : numpy.ndarray + received data, transformed by the incoming virtual ports + """ + if self._transform_funcs is not None: + # apply all transformation functions to the received data + for f in self._transform_funcs: + recv_data = f(recv_data) + return recv_data + class PyInPortVectorDense(PyInPort): def recv(self) -> np.ndarray: return ft.reduce( - lambda acc, csp_port: acc + csp_port.recv(), + lambda acc, csp_port: acc + self._transform(csp_port.recv()), self._csp_recv_ports, np.zeros(self._shape, self._d_type), ) def peek(self) -> np.ndarray: return ft.reduce( - lambda acc, csp_port: acc + csp_port.peek(), + lambda acc, csp_port: acc + self._transform(csp_port.peek()), self._csp_recv_ports, np.zeros(self._shape, self._d_type), ) diff --git a/src/lava/magma/core/process/ports/ports.py b/src/lava/magma/core/process/ports/ports.py index abec55cf2..682e9ad0c 100644 --- a/src/lava/magma/core/process/ports/ports.py +++ b/src/lava/magma/core/process/ports/ports.py @@ -1,8 +1,12 @@ # Copyright (C) 2021 Intel Corporation -# SPDX-License-Identifier: BSD-3-Clause +# SPDX-License-Identifier: BSD-3-Clause +# See: https://spdx.org/licenses/ + import typing as ty from abc import ABC, abstractmethod import math +import numpy as np +import functools as ft from lava.magma.core.process.interfaces import AbstractProcessMember import lava.magma.core.process.ports.exceptions as pe @@ -141,6 +145,26 @@ def get_src_ports(self, _include_self=False) -> ty.List["AbstractPort"]: ports += p.get_src_ports(True) return ports + def get_incoming_virtual_ports(self) -> ty.List["AbstractVirtualPort"]: + """Returns the list of all incoming virtual ports in order from + source to the current port. + + Returns + ------- + virtual_ports : list(AbstractVirtualPorts) + the list of all incoming virtual ports, sorted from source to + destination port + """ + if len(self.in_connections) == 0: + return [] + else: + virtual_ports = [] + for p in self.in_connections: + if isinstance(p, AbstractVirtualPort): + virtual_ports += p.get_incoming_virtual_ports() + virtual_ports.append(p) + return virtual_ports + def get_dst_ports(self, _include_self=False) -> ty.List["AbstractPort"]: """Returns the list of all destination ports that this port connects to either directly or indirectly (through other ports).""" @@ -233,7 +257,8 @@ def transpose( if idx_positive < 0 or idx_positive >= len(self.shape): raise pe.TransposeIndexError(self.shape, axes, idx) - transpose_port = TransposePort(tuple([self.shape[i] for i in axes])) + new_shape = tuple([self.shape[i] for i in axes]) + transpose_port = TransposePort(new_shape, axes) self._connect_forward( [transpose_port], AbstractPort, assert_same_shape=False ) @@ -593,17 +618,14 @@ class ImplicitVarPort(VarPort): pass -class AbstractVirtualPort(ABC): +class AbstractVirtualPort(AbstractPort): """Abstract base class interface for any type of port that merely serves - to transforms the properties of a user-defined port. - Needs no implementation because this class purely serves as a - type-identifier.""" + to transforms the properties of a user-defined port.""" @property - @abstractmethod def _parent_port(self): """Must return parent port that this VirtualPort was derived from.""" - pass + return self.get_src_ports()[0] @property def process(self): @@ -611,24 +633,8 @@ def process(self): derived from.""" return self._parent_port.process - -# ToDo: (AW) ReshapePort.connect(..) could be consolidated with -# ConcatPort.connect(..) -class ReshapePort(AbstractVirtualPort, AbstractPort): - """A ReshapePort is a virtual port that allows to change the shape of a - port before connecting to another port. - It is used by the compiler to map the indices of the underlying - tensor-valued data array from the derived to the new shape.""" - - def __init__(self, shape: ty.Tuple): - AbstractPort.__init__(self, shape) - - @property - def _parent_port(self) -> AbstractPort: - return self.in_connections[0] - def connect(self, ports: ty.Union["AbstractPort", ty.List["AbstractPort"]]): - """Connects this ReshapePort to other port(s). + """Connects this virtual port to other port(s). Parameters ---------- @@ -653,6 +659,23 @@ def connect(self, ports: ty.Union["AbstractPort", ty.List["AbstractPort"]]): # Connect to ports self._connect_forward(to_list(ports), port_type) + @abstractmethod + def get_transform_func(self) -> ft.partial: + pass + + +class ReshapePort(AbstractVirtualPort, AbstractPort): + """A ReshapePort is a virtual port that allows to change the shape of a + port before connecting to another port. + It is used by the compiler to map the indices of the underlying + tensor-valued data array from the derived to the new shape.""" + + def __init__(self, new_shape: ty.Tuple): + AbstractPort.__init__(self, new_shape) + + def get_transform_func(self) -> ft.partial: + return ft.partial(np.reshape, newshape=self.shape) + class ConcatPort(AbstractVirtualPort, AbstractPort): """A ConcatPort is a virtual port that allows to concatenate multiple @@ -695,38 +718,11 @@ def _get_new_shape(ports: ty.List[AbstractPort], axis): new_shape = shapes_ex_axis[0] return new_shape[:axis] + (total_size,) + new_shape[axis:] - @property - def _parent_port(self) -> AbstractPort: - return self.in_connections[0] + def get_transform_func(self) -> ft.partial: + # TODO (MR): not yet implemented + raise NotImplementedError() - def connect(self, ports: ty.Union["AbstractPort", ty.List["AbstractPort"]]): - """Connects this ConcatPort to other port(s) - Parameters - ---------- - :param ports: The port(s) to connect to. Connections from an IOPort - to a RVPort and vice versa are not allowed. - """ - # Determine allows port_type - if isinstance(self._parent_port, OutPort): - # If OutPort, only allow other IO ports - port_type = AbstractIOPort - elif isinstance(self._parent_port, InPort): - # If InPort, only allow other InPorts - port_type = InPort - elif isinstance(self._parent_port, RefPort): - # If RefPort, only allow other Ref- or VarPorts - port_type = AbstractRVPort - elif isinstance(self._parent_port, VarPort): - # If VarPort, only allow other VarPorts - port_type = VarPort - else: - raise TypeError("Illegal parent port.") - # Connect to ports - self._connect_forward(to_list(ports), port_type) - - -# ToDo: TBD... class TransposePort(AbstractVirtualPort, AbstractPort): """A TransposePort is a virtual port that allows to permute the dimensions of a port before connecting to another port. @@ -739,38 +735,14 @@ class TransposePort(AbstractVirtualPort, AbstractPort): out_port.transpose([3, 1, 2]).connect(in_port) """ - def __init__(self, shape: ty.Optional[ty.Tuple] = None): - AbstractPort.__init__(self, shape) + def __init__(self, + new_shape: ty.Tuple[int, ...], + axes: ty.Tuple[int, ...]): + self._axes = axes + AbstractPort.__init__(self, new_shape) - @property - def _parent_port(self) -> AbstractPort: - return self.in_connections[0] - - def connect(self, ports: ty.Union["AbstractPort", ty.List["AbstractPort"]]): - """Connects this ReshapePort to other port(s). - - Parameters - ---------- - :param ports: The port(s) to connect to. Connections from an IOPort - to a RVPort and vice versa are not allowed. - """ - # Determine allows port_type - if isinstance(self._parent_port, OutPort): - # If OutPort, only allow other IO ports - port_type = AbstractIOPort - elif isinstance(self._parent_port, InPort): - # If InPort, only allow other InPorts - port_type = InPort - elif isinstance(self._parent_port, RefPort): - # If RefPort, only allow other Ref- or VarPorts - port_type = AbstractRVPort - elif isinstance(self._parent_port, VarPort): - # If VarPort, only allow other VarPorts - port_type = VarPort - else: - raise TypeError("Illegal parent port.") - # Connect to ports - self._connect_forward(to_list(ports), port_type) + def get_transform_func(self) -> ft.partial: + return ft.partial(np.transpose, axes=self._axes) # ToDo: TBD... diff --git a/tests/lava/magma/core/model/py/test_ports.py b/tests/lava/magma/core/model/py/test_ports.py index a929e135b..372615261 100644 --- a/tests/lava/magma/core/model/py/test_ports.py +++ b/tests/lava/magma/core/model/py/test_ports.py @@ -58,7 +58,7 @@ def probe_test_routine(self, cls): PyOutPortVectorDense([send_csp_port_2], None) # Create PyInPort with current implementation recv_py_port: PyInPort = \ - cls([recv_csp_port_1, recv_csp_port_2], None) + cls([recv_csp_port_1, recv_csp_port_2], None, None) recv_py_port.start() send_py_port_1.start() diff --git a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py index d5bfba7b1..4742f269f 100644 --- a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py +++ b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py @@ -69,6 +69,31 @@ def test_transpose_outport_to_inport(self) -> None: f'{expected[output!=expected] =}\n' ) + def test_transpose_chaining(self) -> None: + """Tests whether two virtual TransposePorts can be chained.""" + + source = OutPortProcess(data=self.input_data) + # transpose the shape once more + self.shape_transposed = tuple(self.shape_transposed[i] for i in + self.axes) + sink = InPortProcess(shape=self.shape_transposed) + + source.out_port.transpose(axes=self.axes).transpose( + axes=self.axes).connect(sink.in_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.transpose(self.axes).transpose(self.axes) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + def test_transpose_inport_to_inport(self) -> None: """Tests a virtual TransposePort between an InPort and another InPort. In a real implementation, the source InPort would be in a @@ -95,6 +120,7 @@ def test_transpose_inport_to_inport(self) -> None: f'{expected[output!=expected] =}\n' ) + @unittest.skip("not yet implemented") def test_transpose_refport_write_to_varport(self) -> None: """Tests a virtual TransposePort between a RefPort and a VarPort, where the RefPort writes to the VarPort.""" @@ -116,6 +142,7 @@ def test_transpose_refport_write_to_varport(self) -> None: f'{expected[output!=expected] =}\n' ) + @unittest.skip("not yet implemented") def test_transpose_refport_read_from_varport(self) -> None: """Tests a virtual TransposePort between a RefPort and a VarPort, where the RefPort reads from the VarPort.""" @@ -137,7 +164,7 @@ def test_transpose_refport_read_from_varport(self) -> None: f'{expected[output!=expected] =}\n' ) - @unittest.skip("RefPort to RefPort not yet implemented") + @unittest.skip("not yet implemented") def test_transpose_refport_write_to_refport(self) -> None: """Tests a virtual TransposePort between a RefPort and another RefPort, where the first RefPort writes to the second. In a real @@ -165,7 +192,7 @@ def test_transpose_refport_write_to_refport(self) -> None: f'{expected[output!=expected] =}\n' ) - @unittest.skip("RefPort to RefPort not yet implemented") + @unittest.skip("not yet implemented") def test_transpose_refport_read_from_refport(self) -> None: """Tests a virtual TransposePort between a RefPort and another RefPort, where the first RefPort reads from the second. In a real @@ -219,7 +246,7 @@ def test_transpose_varport_write_to_varport(self) -> None: f'{expected[output!=expected] =}\n' ) - @unittest.skip("VarPort to VarPort not yet implemented") + @unittest.skip("not yet implemented") def test_transpose_varport_read_from_varport(self) -> None: """Tests a virtual TransposePort between a VarPort and another VarPort, where the first VarPort reads from the second. In a real @@ -277,6 +304,29 @@ def test_reshape_outport_to_inport(self) -> None: f'{expected[output!=expected] =}\n' ) + def test_reshape_chaining(self) -> None: + """Tests whether two virtual ReshapePorts can be chained.""" + + source = OutPortProcess(data=self.input_data) + shape_final = (int(np.prod(self.shape)),) + sink = InPortProcess(shape=shape_final) + + source.out_port.reshape(self.shape_reshaped).reshape( + shape_final).connect(sink.in_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.reshape(shape_final) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + def test_reshape_inport_to_inport(self) -> None: """Tests a virtual ReshapePort between an InPort and another InPort. In a real implementation, the source InPort would be in a @@ -304,6 +354,7 @@ def test_reshape_inport_to_inport(self) -> None: f'{expected[output!=expected] =}\n' ) + @unittest.skip("not yet implemented") def test_reshape_refport_write_to_varport(self) -> None: """Tests a virtual ReshapePort between a RefPort and a VarPort, where the RefPort writes to the VarPort.""" @@ -326,6 +377,7 @@ def test_reshape_refport_write_to_varport(self) -> None: f'{expected[output!=expected] =}\n' ) + @unittest.skip("not yet implemented") def test_reshape_refport_read_from_varport(self) -> None: """Tests a virtual ReshapePort between a RefPort and a VarPort, where the RefPort reads from the VarPort.""" @@ -350,7 +402,8 @@ def test_reshape_refport_read_from_varport(self) -> None: class TestFlattenPort(unittest.TestCase): - """Tests virtual FlattenPorts on Processes that are executed.""" + """Tests virtual ReshapePorts, created by the flatten() method, + on Processes that are executed.""" def setUp(self) -> None: self.num_steps = 1 @@ -359,7 +412,8 @@ def setUp(self) -> None: self.input_data = np.random.randint(256, size=self.shape) def test_flatten_outport_to_inport(self) -> None: - """Tests a virtual FlattenPort between an OutPort and an InPort.""" + """Tests a virtual ReshapePort with flatten() between an OutPort and an + InPort.""" source = OutPortProcess(data=self.input_data) sink = InPortProcess(shape=self.shape_reshaped) @@ -379,11 +433,33 @@ def test_flatten_outport_to_inport(self) -> None: f'{expected[output!=expected] =}\n' ) + def test_flatten_chaining(self) -> None: + """Tests whether two virtual ReshapePorts can be chained through the + flatten() method.""" + + source = OutPortProcess(data=self.input_data) + sink = InPortProcess(shape=self.shape_reshaped) + + source.out_port.flatten().flatten().connect(sink.in_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.ravel() + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + def test_flatten_inport_to_inport(self) -> None: - """Tests a virtual FlattenPort between an InPort and another InPort. - In a real implementation, the source InPort would be in a - hierarchical Process and the sink InPort would be in a SubProcess of - that hierarchical Process.""" + """Tests a virtual ReshapePort with flatten() between an InPort and + another InPort. In a real implementation, the source InPort would be + in a hierarchical Process and the sink InPort would be in a SubProcess + of that hierarchical Process.""" out_port_process = OutPortProcess(data=self.input_data) source = InPortProcess(shape=self.shape) @@ -405,9 +481,10 @@ def test_flatten_inport_to_inport(self) -> None: f'{expected[output!=expected] =}\n' ) + @unittest.skip("not yet implemented") def test_flatten_refport_write_to_varport(self) -> None: - """Tests a virtual FlattenPort between a RefPort and a VarPort, - where the RefPort writes to the VarPort.""" + """Tests a virtual ReshapePort with flatten() between a RefPort and a + VarPort, where the RefPort writes to the VarPort.""" source = RefPortWriteProcess(data=self.input_data) sink = VarPortProcess(data=np.zeros(self.shape_reshaped)) @@ -426,9 +503,10 @@ def test_flatten_refport_write_to_varport(self) -> None: f'{expected[output!=expected] =}\n' ) + @unittest.skip("not yet implemented") def test_flatten_refport_read_from_varport(self) -> None: - """Tests a virtual FlattenPort between a RefPort and a VarPort, - where the RefPort reads from the VarPort.""" + """Tests a virtual ReshapePort with flatten() between a RefPort and a + VarPort, where the RefPort reads from the VarPort.""" source = VarPortProcess(data=self.input_data) sink = RefPortReadProcess(data=np.zeros(self.shape_reshaped)) @@ -457,6 +535,7 @@ def setUp(self) -> None: self.shape_concat = (3, 3, 4) self.input_data = np.random.randint(256, size=self.shape) + @unittest.skip("not yet implemented") def test_concat_outport_to_inport(self) -> None: """Tests a virtual ConcatPort between an OutPort and an InPort.""" @@ -483,7 +562,7 @@ def test_concat_outport_to_inport(self) -> None: ) -# minimal process with an OutPort +# minimal Process with an OutPort class OutPortProcess(AbstractProcess): def __init__(self, data: np.ndarray) -> None: super().__init__(data=data) @@ -491,7 +570,7 @@ def __init__(self, data: np.ndarray) -> None: self.out_port = OutPort(shape=data.shape) -# minimal process with an InPort +# minimal Process with an InPort class InPortProcess(AbstractProcess): def __init__(self, shape: ty.Tuple[int, ...]) -> None: super().__init__(shape=shape) @@ -499,7 +578,7 @@ def __init__(self, shape: ty.Tuple[int, ...]) -> None: self.in_port = InPort(shape=shape) -# A minimal process with a RefPort that writes +# A minimal Process with a RefPort that writes class RefPortWriteProcess(AbstractProcess): def __init__(self, data: np.ndarray) -> None: super().__init__(data=data) @@ -507,7 +586,7 @@ def __init__(self, data: np.ndarray) -> None: self.ref_port = RefPort(shape=data.shape) -# A minimal process with a RefPort that reads +# A minimal Process with a RefPort that reads class RefPortReadProcess(AbstractProcess): def __init__(self, data: np.ndarray) -> None: super().__init__(data=data) @@ -515,7 +594,7 @@ def __init__(self, data: np.ndarray) -> None: self.ref_port = RefPort(shape=data.shape) -# A minimal process with a Var and a VarPort +# A minimal Process with a Var and a VarPort class VarPortProcess(AbstractProcess): def __init__(self, data: np.ndarray) -> None: super().__init__(data=data) From 846c1b3e34a6ba395013e441f36e998b850e6fcc Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Thu, 17 Feb 2022 22:40:40 +0100 Subject: [PATCH 10/21] Fixing unit tests after merge Signed-off-by: Mathis Richter --- src/lava/magma/compiler/builders/builder.py | 4 ++-- src/lava/magma/core/model/py/ports.py | 13 +++++++------ tests/lava/magma/core/model/py/test_ports.py | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/lava/magma/compiler/builders/builder.py b/src/lava/magma/compiler/builders/builder.py index b2c24bd48..b2737943a 100644 --- a/src/lava/magma/compiler/builders/builder.py +++ b/src/lava/magma/compiler/builders/builder.py @@ -350,8 +350,8 @@ def build(self): # TODO (MR): This is probably just a temporary hack until the # interface of PyOutPorts has been adjusted. if issubclass(port_cls, PyInPort): - port = port_cls(csp_ports, p.transform_funcs, pm, p.shape, - lt.d_type) + port = port_cls(csp_ports, pm, p.shape, lt.d_type, + p.transform_funcs) elif issubclass(port_cls, PyOutPort): port = port_cls(csp_ports, pm, p.shape, lt.d_type) else: diff --git a/src/lava/magma/core/model/py/ports.py b/src/lava/magma/core/model/py/ports.py index ee5dced34..d8e28c747 100644 --- a/src/lava/magma/core/model/py/ports.py +++ b/src/lava/magma/core/model/py/ports.py @@ -139,13 +139,14 @@ class PyInPort(AbstractPyIOPort): SCALAR_SPARSE: ty.Type["PyInPortScalarSparse"] = None def __init__(self, - csp_recv_ports: ty.List[CspRecvPort], - transform_funcs: ty.Optional[ty.List[ft.partial]] = None, - *args): - self._csp_recv_ports = csp_recv_ports - self._transform_funcs = transform_funcs + csp_ports: ty.List[AbstractCspPort], + process_model: AbstractProcessModel, + shape: ty.Tuple[int, ...], + d_type: type, + transform_funcs: ty.Optional[ty.List[ft.partial]] = None): - super().__init__(*args) + self._transform_funcs = transform_funcs + super().__init__(csp_ports, process_model, shape, d_type) @abstractmethod def recv(self): diff --git a/tests/lava/magma/core/model/py/test_ports.py b/tests/lava/magma/core/model/py/test_ports.py index a4b3a3597..1a59e7dd4 100644 --- a/tests/lava/magma/core/model/py/test_ports.py +++ b/tests/lava/magma/core/model/py/test_ports.py @@ -60,8 +60,8 @@ def probe_test_routine(self, cls): data.dtype) # Create PyInPort with current implementation recv_py_port: PyInPort = \ - cls([recv_csp_port_1, recv_csp_port_2], None, None, data.shape, - data.dtype) + cls([recv_csp_port_1, recv_csp_port_2], None, data.shape, + data.dtype, None) recv_py_port.start() send_py_port_1.start() From effd5229999f1e821015107484b747c1c3b3fc3f Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Tue, 22 Feb 2022 17:40:05 +0100 Subject: [PATCH 11/21] Added support for virtual ports between an OutPort and InPort of two hierarchical Processes Signed-off-by: Mathis Richter --- src/lava/magma/core/model/py/ports.py | 2 +- src/lava/magma/core/process/ports/ports.py | 4 +- .../ports/test_virtual_ports_in_process.py | 111 +++++++++++++++++- 3 files changed, 112 insertions(+), 5 deletions(-) diff --git a/src/lava/magma/core/model/py/ports.py b/src/lava/magma/core/model/py/ports.py index d8e28c747..6a3c6ffa2 100644 --- a/src/lava/magma/core/model/py/ports.py +++ b/src/lava/magma/core/model/py/ports.py @@ -206,7 +206,7 @@ def _transform(self, recv_data: np.array) -> np.array: recv_data : numpy.ndarray received data, transformed by the incoming virtual ports """ - if self._transform_funcs is not None: + if self._transform_funcs: # apply all transformation functions to the received data for f in self._transform_funcs: recv_data = f(recv_data) diff --git a/src/lava/magma/core/process/ports/ports.py b/src/lava/magma/core/process/ports/ports.py index 18bf68411..5abb388f8 100644 --- a/src/lava/magma/core/process/ports/ports.py +++ b/src/lava/magma/core/process/ports/ports.py @@ -160,8 +160,8 @@ def get_incoming_virtual_ports(self) -> ty.List["AbstractVirtualPort"]: else: virtual_ports = [] for p in self.in_connections: + virtual_ports += p.get_incoming_virtual_ports() if isinstance(p, AbstractVirtualPort): - virtual_ports += p.get_incoming_virtual_ports() virtual_ports.append(p) return virtual_ports @@ -628,7 +628,7 @@ class ImplicitVarPort(VarPort): class AbstractVirtualPort(AbstractPort): """Abstract base class interface for any type of port that merely serves - to transforms the properties of a user-defined port.""" + to transform the properties of a user-defined port.""" @property def _parent_port(self): diff --git a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py index 4742f269f..c1f53caa9 100644 --- a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py +++ b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py @@ -8,6 +8,7 @@ from lava.magma.core.decorator import requires, tag, implements from lava.magma.core.model.py.model import PyLoihiProcessModel +from lava.magma.core.model.sub.model import AbstractSubProcessModel from lava.magma.core.model.py.type import LavaPyType from lava.magma.core.process.variable import Var from lava.magma.core.process.process import AbstractProcess @@ -69,6 +70,29 @@ def test_transpose_outport_to_inport(self) -> None: f'{expected[output!=expected] =}\n' ) + def test_transpose_outport_to_inport_hierarchical(self) -> None: + """Tests a virtual TransposePort between an OutPort + of a hierarchical Process and an InPort of another hierarchical + Process.""" + + source = HOutPortProcess(data=self.input_data) + sink = HInPortProcess(shape=self.shape_transposed) + + source.out_port.transpose(axes=self.axes).connect(sink.in_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.transpose(self.axes) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + def test_transpose_chaining(self) -> None: """Tests whether two virtual TransposePorts can be chained.""" @@ -304,6 +328,30 @@ def test_reshape_outport_to_inport(self) -> None: f'{expected[output!=expected] =}\n' ) + def test_reshape_outport_to_inport_hierarchical(self) -> None: + """Tests a virtual ReshapePort between an OutPort + of a hierarchical Process and an InPort of another hierarchical + Process.""" + + source = HOutPortProcess(data=self.input_data) + sink = HInPortProcess(shape=self.shape_reshaped) + + source.out_port.reshape(new_shape=self.shape_reshaped).connect( + sink.in_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.reshape(self.shape_reshaped) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + def test_reshape_chaining(self) -> None: """Tests whether two virtual ReshapePorts can be chained.""" @@ -433,6 +481,29 @@ def test_flatten_outport_to_inport(self) -> None: f'{expected[output!=expected] =}\n' ) + def test_flatten_outport_to_inport_hierarchical(self) -> None: + """Tests a virtual ReshapePort with flatten() between an OutPort + of a hierarchical Process and an InPort of another hierarchical + Process.""" + + source = HOutPortProcess(data=self.input_data) + sink = HInPortProcess(shape=self.shape_reshaped) + + source.out_port.flatten().connect(sink.in_port) + + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + sink.stop() + + expected = self.input_data.ravel() + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + def test_flatten_chaining(self) -> None: """Tests whether two virtual ReshapePorts can be chained through the flatten() method.""" @@ -562,7 +633,7 @@ def test_concat_outport_to_inport(self) -> None: ) -# minimal Process with an OutPort +# A minimal Process with an OutPort class OutPortProcess(AbstractProcess): def __init__(self, data: np.ndarray) -> None: super().__init__(data=data) @@ -570,7 +641,7 @@ def __init__(self, data: np.ndarray) -> None: self.out_port = OutPort(shape=data.shape) -# minimal Process with an InPort +# A minimal Process with an InPort class InPortProcess(AbstractProcess): def __init__(self, shape: ty.Tuple[int, ...]) -> None: super().__init__(shape=shape) @@ -578,6 +649,24 @@ def __init__(self, shape: ty.Tuple[int, ...]) -> None: self.in_port = InPort(shape=shape) +# A minimal hierarchical Process with an OutPort +class HOutPortProcess(AbstractProcess): + def __init__(self, data: np.ndarray) -> None: + super().__init__(data=data) + self.data = Var(shape=data.shape, init=data) + self.out_port = OutPort(shape=data.shape) + self.proc_params['data'] = data + + +# A minimal hierarchical Process with an InPort and a Var +class HInPortProcess(AbstractProcess): + def __init__(self, shape: ty.Tuple[int, ...]) -> None: + super().__init__(shape=shape) + self.data = Var(shape=shape, init=np.zeros(shape)) + self.in_port = InPort(shape=shape) + self.proc_params['shape'] = shape + + # A minimal Process with a RefPort that writes class RefPortWriteProcess(AbstractProcess): def __init__(self, data: np.ndarray) -> None: @@ -669,5 +758,23 @@ class PyVarPortProcessModelFloat(PyLoihiProcessModel): data: np.ndarray = LavaPyType(np.ndarray, np.int32) +# A minimal hierarchical ProcModel with a nested OutPortProcess +@implements(proc=HOutPortProcess) +class SubHOutPortProcModel(AbstractSubProcessModel): + def __init__(self, proc): + self.out_proc = OutPortProcess(data=proc.proc_params['data']) + self.out_proc.out_port.connect(proc.out_port) + + +# A minimal hierarchical ProcModel with a nested InPortProcess and an aliased +# Var +@implements(proc=HInPortProcess) +class SubHInPortProcModel(AbstractSubProcessModel): + def __init__(self, proc): + self.in_proc = InPortProcess(shape=proc.proc_params['shape']) + proc.in_port.connect(self.in_proc.in_port) + proc.data.alias(self.in_proc.data) + + if __name__ == '__main__': unittest.main() From c5d0f5f1e2d62dbb78d335473bf67df0685a60ad Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Tue, 22 Feb 2022 22:51:49 +0100 Subject: [PATCH 12/21] Clean up, exceptions, and generic unit tests for virtual port topologies Signed-off-by: Mathis Richter --- src/lava/magma/core/process/ports/ports.py | 27 +- .../ports/test_virtual_ports_in_process.py | 612 +++--------------- 2 files changed, 99 insertions(+), 540 deletions(-) diff --git a/src/lava/magma/core/process/ports/ports.py b/src/lava/magma/core/process/ports/ports.py index 5abb388f8..bd505dc9c 100644 --- a/src/lava/magma/core/process/ports/ports.py +++ b/src/lava/magma/core/process/ports/ports.py @@ -159,10 +159,17 @@ def get_incoming_virtual_ports(self) -> ty.List["AbstractVirtualPort"]: return [] else: virtual_ports = [] + num_virtual_ports = 0 for p in self.in_connections: virtual_ports += p.get_incoming_virtual_ports() if isinstance(p, AbstractVirtualPort): virtual_ports.append(p) + num_virtual_ports += 1 + + if num_virtual_ports > 1: + raise NotImplementedError("joining multiple virtual ports is " + "not yet supported") + return virtual_ports def get_dst_ports(self, _include_self=False) -> ty.List["AbstractPort"]: @@ -189,6 +196,12 @@ def reshape(self, new_shape: ty.Tuple) -> "ReshapePort": :param new_shape: New shape of port. Number of total elements must not change. """ + # TODO (MR): Implement for other types of Ports + if not (isinstance(self, OutPort) or + isinstance(self, AbstractVirtualPort)): + raise NotImplementedError("reshape/flatten are only implemented " + "for OutPorts") + if self.size != math.prod(new_shape): raise pe.ReshapeError(self.shape, new_shape) @@ -242,6 +255,12 @@ def transpose( :param axes: Order of permutation. Number of total elements and number of dimensions must not change. """ + # TODO (MR): Implement for other types of Ports + if not (isinstance(self, OutPort) or + isinstance(self, AbstractVirtualPort)): + raise NotImplementedError("transpose is only implemented for " + "OutPorts") + if axes is None: axes = tuple(reversed(range(len(self.shape)))) else: @@ -672,7 +691,7 @@ def get_transform_func(self) -> ft.partial: pass -class ReshapePort(AbstractVirtualPort, AbstractPort): +class ReshapePort(AbstractVirtualPort): """A ReshapePort is a virtual port that allows to change the shape of a port before connecting to another port. It is used by the compiler to map the indices of the underlying @@ -685,7 +704,7 @@ def get_transform_func(self) -> ft.partial: return ft.partial(np.reshape, newshape=self.shape) -class ConcatPort(AbstractVirtualPort, AbstractPort): +class ConcatPort(AbstractVirtualPort): """A ConcatPort is a virtual port that allows to concatenate multiple ports along given axis into a new port before connecting to another port. The shape of all concatenated ports outside of the concatenation @@ -731,7 +750,7 @@ def get_transform_func(self) -> ft.partial: raise NotImplementedError() -class TransposePort(AbstractVirtualPort, AbstractPort): +class TransposePort(AbstractVirtualPort): """A TransposePort is a virtual port that allows to permute the dimensions of a port before connecting to another port. It is used by the compiler to map the indices of the underlying @@ -754,7 +773,7 @@ def get_transform_func(self) -> ft.partial: # ToDo: TBD... -class ReIndexPort(AbstractVirtualPort, AbstractPort): +class ReIndexPort(AbstractVirtualPort): """A ReIndexPort is a virtual port that allows to re-index the elements of a port before connecting to another port. It is used by the compiler to map the indices of the underlying diff --git a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py index c1f53caa9..474146fd5 100644 --- a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py +++ b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py @@ -5,6 +5,7 @@ import typing as ty import unittest import numpy as np +import functools as ft from lava.magma.core.decorator import requires, tag, implements from lava.magma.core.model.py.model import PyLoihiProcessModel @@ -18,74 +19,58 @@ from lava.magma.core.run_configs import Loihi1SimCfg from lava.magma.core.model.py.ports import ( PyInPort, - PyOutPort, - PyRefPort, - PyVarPort + PyOutPort ) from lava.magma.core.process.ports.ports import ( + AbstractPort, + AbstractVirtualPort, InPort, - OutPort, - RefPort, - VarPort + OutPort ) np.random.seed(7739) -class TestTransposePort(unittest.TestCase): - """Tests virtual TransposePorts on Processes that are executed.""" +class MockVirtualPort(AbstractVirtualPort, AbstractPort): + """A mock-up of a virtual port that reshapes the input.""" + def __init__(self, new_shape: ty.Tuple): + AbstractPort.__init__(self, new_shape) + + def get_transform_func(self) -> ft.partial: + return ft.partial(np.reshape, newshape=self.shape) + + +class TestVirtualPortNetworkTopologies(unittest.TestCase): + """Tests different network topologies that include virtual ports using a + dummy virtual port as a stand-in for all types of virtual ports.""" def setUp(self) -> None: self.num_steps = 1 - self.axes = (2, 0, 1) - self.axes_reverse = list(self.axes) - for idx, ax in enumerate(self.axes): - self.axes_reverse[ax] = idx - self.axes_reverse = tuple(self.axes_reverse) self.shape = (4, 3, 2) - self.shape_transposed = tuple(self.shape[i] for i in self.axes) - self.shape_transposed_reverse = \ - tuple(self.shape[i] for i in self.axes_reverse) + self.new_shape = (12, 2) self.input_data = np.random.randint(256, size=self.shape) - def test_transpose_outport_to_inport(self) -> None: - """Tests a virtual TransposePort between an OutPort and an InPort.""" - - source = OutPortProcess(data=self.input_data) - sink = InPortProcess(shape=self.shape_transposed) + def test_virtual_ports_between_hierarchical_processes(self) -> None: + """Tests a virtual port between an OutPort of a hierarchical Process + and an InPort of another hierarchical Process.""" - source.out_port.transpose(axes=self.axes).connect(sink.in_port) + source = HOutPortProcess(data=self.input_data) + sink = HInPortProcess(shape=self.new_shape) - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() + virtual_port = MockVirtualPort(new_shape=self.new_shape) - expected = self.input_data.transpose(self.axes) - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' + source.out_port._connect_forward( + [virtual_port], AbstractPort, assert_same_shape=False ) - - def test_transpose_outport_to_inport_hierarchical(self) -> None: - """Tests a virtual TransposePort between an OutPort - of a hierarchical Process and an InPort of another hierarchical - Process.""" - - source = HOutPortProcess(data=self.input_data) - sink = HInPortProcess(shape=self.shape_transposed) - - source.out_port.transpose(axes=self.axes).connect(sink.in_port) + virtual_port.connect(sink.in_port) sink.run(condition=RunSteps(num_steps=self.num_steps), run_cfg=Loihi1SimCfg(select_tag='floating_pt')) output = sink.data.get() sink.stop() - expected = self.input_data.transpose(self.axes) + expected = self.input_data.reshape(self.new_shape) self.assertTrue( np.all(output == expected), f'Input and output do not match.\n' @@ -93,72 +78,31 @@ def test_transpose_outport_to_inport_hierarchical(self) -> None: f'{expected[output!=expected] =}\n' ) - def test_transpose_chaining(self) -> None: - """Tests whether two virtual TransposePorts can be chained.""" + def test_chaining_multiple_virtual_ports(self) -> None: + """Tests whether two virtual ReshapePorts can be chained through the + flatten() method.""" source = OutPortProcess(data=self.input_data) - # transpose the shape once more - self.shape_transposed = tuple(self.shape_transposed[i] for i in - self.axes) - sink = InPortProcess(shape=self.shape_transposed) - - source.out_port.transpose(axes=self.axes).transpose( - axes=self.axes).connect(sink.in_port) + shape_final = (int(np.prod(self.shape)),) + sink = InPortProcess(shape=shape_final) - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() + virtual_port1 = MockVirtualPort(new_shape=self.new_shape) + virtual_port2 = MockVirtualPort(new_shape=shape_final) - expected = self.input_data.transpose(self.axes).transpose(self.axes) - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' + source.out_port._connect_forward( + [virtual_port1], AbstractPort, assert_same_shape=False ) - - def test_transpose_inport_to_inport(self) -> None: - """Tests a virtual TransposePort between an InPort and another InPort. - In a real implementation, the source InPort would be in a - hierarchical Process and the sink InPort would be in a SubProcess of - that hierarchical Process.""" - - out_port_process = OutPortProcess(data=self.input_data) - source = InPortProcess(shape=self.shape) - sink = InPortProcess(shape=self.shape_transposed) - - out_port_process.out_port.connect(source.in_port) - source.in_port.transpose(axes=self.axes).connect(sink.in_port) - - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() - - expected = self.input_data.transpose(self.axes) - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' + virtual_port1._connect_forward( + [virtual_port2], AbstractPort, assert_same_shape=False ) - - @unittest.skip("not yet implemented") - def test_transpose_refport_write_to_varport(self) -> None: - """Tests a virtual TransposePort between a RefPort and a VarPort, - where the RefPort writes to the VarPort.""" - - source = RefPortWriteProcess(data=self.input_data) - sink = VarPortProcess(data=np.zeros(self.shape_transposed)) - source.ref_port.transpose(axes=self.axes).connect(sink.var_port) + virtual_port2.connect(sink.in_port) sink.run(condition=RunSteps(num_steps=self.num_steps), run_cfg=Loihi1SimCfg(select_tag='floating_pt')) output = sink.data.get() sink.stop() - expected = self.input_data.transpose(self.axes) + expected = self.input_data.ravel() self.assertTrue( np.all(output == expected), f'Input and output do not match.\n' @@ -166,96 +110,54 @@ def test_transpose_refport_write_to_varport(self) -> None: f'{expected[output!=expected] =}\n' ) - @unittest.skip("not yet implemented") - def test_transpose_refport_read_from_varport(self) -> None: - """Tests a virtual TransposePort between a RefPort and a VarPort, - where the RefPort reads from the VarPort.""" + def test_joining_virtual_ports_throws_exception(self) -> None: + """Tests whether joining two virtual ports throws an exception.""" - source = VarPortProcess(data=self.input_data) - sink = RefPortReadProcess(data=np.zeros(self.shape_transposed_reverse)) - sink.ref_port.transpose(axes=self.axes).connect(source.var_port) + source1 = OutPortProcess(data=self.input_data) + source2 = OutPortProcess(data=self.input_data) + sink = InPortProcess(shape=self.new_shape) - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() + virtual_port1 = MockVirtualPort(new_shape=self.new_shape) + virtual_port2 = MockVirtualPort(new_shape=self.new_shape) - expected = self.input_data.transpose(self.axes_reverse) - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' + source1.out_port._connect_forward( + [virtual_port1], AbstractPort, assert_same_shape=False ) - - @unittest.skip("not yet implemented") - def test_transpose_refport_write_to_refport(self) -> None: - """Tests a virtual TransposePort between a RefPort and another - RefPort, where the first RefPort writes to the second. In a real - implementation, the source RefPort would be in a - hierarchical Process and the sink RefPort would be in a SubProcess of - that hierarchical Process.""" - - source = RefPortWriteProcess(data=self.input_data) - sink = RefPortReadProcess(data=np.zeros(self.shape_transposed)) - var_port_process = VarPortProcess(data=np.zeros(self.shape_transposed)) - - source.ref_port.transpose(axes=self.axes).connect(sink.ref_port) - sink.ref_port.connect(var_port_process.var_port) - - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = var_port_process.data.get() - sink.stop() - - expected = self.input_data.transpose(self.axes) - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' + source2.out_port._connect_forward( + [virtual_port2], AbstractPort, assert_same_shape=False ) - @unittest.skip("not yet implemented") - def test_transpose_refport_read_from_refport(self) -> None: - """Tests a virtual TransposePort between a RefPort and another - RefPort, where the first RefPort reads from the second. In a real - implementation, the source RefPort would be in a - hierarchical Process and the sink RefPort would be in a SubProcess of - that hierarchical Process.""" + virtual_port1.connect(sink.in_port) + virtual_port2.connect(sink.in_port) - source = RefPortReadProcess( - data=np.zeros(self.shape_transposed_reverse) - ) - sink = RefPortWriteProcess(data=self.input_data) + with self.assertRaises(NotImplementedError): + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - sink.ref_port.transpose(axes=self.axes).connect(source.ref_port) - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() +class TestTransposePort(unittest.TestCase): + """Tests virtual TransposePorts on Processes that are executed.""" - expected = self.input_data.transpose(self.axes_reverse) - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' - ) + def setUp(self) -> None: + self.num_steps = 1 + self.axes = (2, 0, 1) + self.axes_reverse = list(self.axes) + for idx, ax in enumerate(self.axes): + self.axes_reverse[ax] = idx + self.axes_reverse = tuple(self.axes_reverse) + self.shape = (4, 3, 2) + self.shape_transposed = tuple(self.shape[i] for i in self.axes) + self.shape_transposed_reverse = \ + tuple(self.shape[i] for i in self.axes_reverse) + self.input_data = np.random.randint(256, size=self.shape) - @unittest.skip("VarPort to VarPort not yet implemented") - def test_transpose_varport_write_to_varport(self) -> None: - """Tests a virtual TransposePort between a VarPort and another - VarPort, where the first VarPort writes to the second. In a - real implementation, the source VarPort would be in a - hierarchical Process and the sink VarPort would be in a SubProcess of - that hierarchical Process.""" + def test_transpose_outport_to_inport(self) -> None: + """Tests a virtual TransposePort between an OutPort and an InPort.""" - source = VarPortProcess(data=self.input_data) - sink = VarPortProcess(data=np.zeros(self.shape_transposed)) + source = OutPortProcess(data=self.input_data) + sink = InPortProcess(shape=self.shape_transposed) - source.var_port.transpose(axes=self.axes).connect(sink.var_port) + source.out_port.transpose(axes=self.axes).connect(sink.in_port) sink.run(condition=RunSteps(num_steps=self.num_steps), run_cfg=Loihi1SimCfg(select_tag='floating_pt')) @@ -270,32 +172,6 @@ def test_transpose_varport_write_to_varport(self) -> None: f'{expected[output!=expected] =}\n' ) - @unittest.skip("not yet implemented") - def test_transpose_varport_read_from_varport(self) -> None: - """Tests a virtual TransposePort between a VarPort and another - VarPort, where the first VarPort reads from the second. In a real - implementation, the source VarPort would be in a - hierarchical Process and the sink VarPort would be in a SubProcess of - that hierarchical Process.""" - - sink = VarPortProcess(data=np.zeros(self.shape_transposed_reverse)) - source = VarPortProcess(data=self.input_data) - - sink.var_port.transpose(axes=self.axes).connect(source.var_port) - - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() - - expected = self.input_data.transpose(self.axes_reverse) - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' - ) - class TestReshapePort(unittest.TestCase): """Tests virtual ReshapePorts on Processes that are executed.""" @@ -328,126 +204,6 @@ def test_reshape_outport_to_inport(self) -> None: f'{expected[output!=expected] =}\n' ) - def test_reshape_outport_to_inport_hierarchical(self) -> None: - """Tests a virtual ReshapePort between an OutPort - of a hierarchical Process and an InPort of another hierarchical - Process.""" - - source = HOutPortProcess(data=self.input_data) - sink = HInPortProcess(shape=self.shape_reshaped) - - source.out_port.reshape(new_shape=self.shape_reshaped).connect( - sink.in_port) - - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() - - expected = self.input_data.reshape(self.shape_reshaped) - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' - ) - - def test_reshape_chaining(self) -> None: - """Tests whether two virtual ReshapePorts can be chained.""" - - source = OutPortProcess(data=self.input_data) - shape_final = (int(np.prod(self.shape)),) - sink = InPortProcess(shape=shape_final) - - source.out_port.reshape(self.shape_reshaped).reshape( - shape_final).connect(sink.in_port) - - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() - - expected = self.input_data.reshape(shape_final) - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' - ) - - def test_reshape_inport_to_inport(self) -> None: - """Tests a virtual ReshapePort between an InPort and another InPort. - In a real implementation, the source InPort would be in a - hierarchical Process and the sink InPort would be in a SubProcess of - that hierarchical Process.""" - - out_port_process = OutPortProcess(data=self.input_data) - source = InPortProcess(shape=self.shape) - sink = InPortProcess(shape=self.shape_reshaped) - - out_port_process.out_port.connect(source.in_port) - source.in_port.reshape(new_shape=self.shape_reshaped).connect( - sink.in_port) - - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() - - expected = self.input_data.reshape(self.shape_reshaped) - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' - ) - - @unittest.skip("not yet implemented") - def test_reshape_refport_write_to_varport(self) -> None: - """Tests a virtual ReshapePort between a RefPort and a VarPort, - where the RefPort writes to the VarPort.""" - - source = RefPortWriteProcess(data=self.input_data) - sink = VarPortProcess(data=np.zeros(self.shape_reshaped)) - source.ref_port.reshape(new_shape=self.shape_reshaped).connect( - sink.var_port) - - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() - - expected = self.input_data.reshape(self.shape_reshaped) - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' - ) - - @unittest.skip("not yet implemented") - def test_reshape_refport_read_from_varport(self) -> None: - """Tests a virtual ReshapePort between a RefPort and a VarPort, - where the RefPort reads from the VarPort.""" - - source = VarPortProcess(data=self.input_data) - sink = RefPortReadProcess(data=np.zeros(self.shape_reshaped)) - sink.ref_port.reshape(new_shape=self.shape_reshaped).connect( - source.var_port) - - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() - - expected = self.input_data.reshape(self.shape_reshaped) - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' - ) - class TestFlattenPort(unittest.TestCase): """Tests virtual ReshapePorts, created by the flatten() method, @@ -481,157 +237,6 @@ def test_flatten_outport_to_inport(self) -> None: f'{expected[output!=expected] =}\n' ) - def test_flatten_outport_to_inport_hierarchical(self) -> None: - """Tests a virtual ReshapePort with flatten() between an OutPort - of a hierarchical Process and an InPort of another hierarchical - Process.""" - - source = HOutPortProcess(data=self.input_data) - sink = HInPortProcess(shape=self.shape_reshaped) - - source.out_port.flatten().connect(sink.in_port) - - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() - - expected = self.input_data.ravel() - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' - ) - - def test_flatten_chaining(self) -> None: - """Tests whether two virtual ReshapePorts can be chained through the - flatten() method.""" - - source = OutPortProcess(data=self.input_data) - sink = InPortProcess(shape=self.shape_reshaped) - - source.out_port.flatten().flatten().connect(sink.in_port) - - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() - - expected = self.input_data.ravel() - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' - ) - - def test_flatten_inport_to_inport(self) -> None: - """Tests a virtual ReshapePort with flatten() between an InPort and - another InPort. In a real implementation, the source InPort would be - in a hierarchical Process and the sink InPort would be in a SubProcess - of that hierarchical Process.""" - - out_port_process = OutPortProcess(data=self.input_data) - source = InPortProcess(shape=self.shape) - sink = InPortProcess(shape=self.shape_reshaped) - - out_port_process.out_port.connect(source.in_port) - source.in_port.flatten().connect(sink.in_port) - - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() - - expected = self.input_data.ravel() - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' - ) - - @unittest.skip("not yet implemented") - def test_flatten_refport_write_to_varport(self) -> None: - """Tests a virtual ReshapePort with flatten() between a RefPort and a - VarPort, where the RefPort writes to the VarPort.""" - - source = RefPortWriteProcess(data=self.input_data) - sink = VarPortProcess(data=np.zeros(self.shape_reshaped)) - source.ref_port.flatten().connect(sink.var_port) - - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() - - expected = self.input_data.ravel() - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' - ) - - @unittest.skip("not yet implemented") - def test_flatten_refport_read_from_varport(self) -> None: - """Tests a virtual ReshapePort with flatten() between a RefPort and a - VarPort, where the RefPort reads from the VarPort.""" - - source = VarPortProcess(data=self.input_data) - sink = RefPortReadProcess(data=np.zeros(self.shape_reshaped)) - sink.ref_port.flatten().connect(source.var_port) - - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() - - expected = self.input_data.ravel() - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' - ) - - -class TestConcatPort(unittest.TestCase): - """Tests virtual ConcatPorts on Processes that are executed.""" - - def setUp(self) -> None: - self.num_steps = 1 - self.shape = (3, 1, 4) - self.shape_concat = (3, 3, 4) - self.input_data = np.random.randint(256, size=self.shape) - - @unittest.skip("not yet implemented") - def test_concat_outport_to_inport(self) -> None: - """Tests a virtual ConcatPort between an OutPort and an InPort.""" - - source_1 = OutPortProcess(data=self.input_data) - source_2 = OutPortProcess(data=self.input_data) - source_3 = OutPortProcess(data=self.input_data) - sink = InPortProcess(shape=self.shape_concat) - - source_1.out_port.concat_with([ - source_2.out_port, - source_3.out_port], axis=1).connect(sink.in_port) - - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() - - expected = np.concatenate([self.input_data] * 3, axis=1) - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' - ) - # A minimal Process with an OutPort class OutPortProcess(AbstractProcess): @@ -667,30 +272,6 @@ def __init__(self, shape: ty.Tuple[int, ...]) -> None: self.proc_params['shape'] = shape -# A minimal Process with a RefPort that writes -class RefPortWriteProcess(AbstractProcess): - def __init__(self, data: np.ndarray) -> None: - super().__init__(data=data) - self.data = Var(shape=data.shape, init=data) - self.ref_port = RefPort(shape=data.shape) - - -# A minimal Process with a RefPort that reads -class RefPortReadProcess(AbstractProcess): - def __init__(self, data: np.ndarray) -> None: - super().__init__(data=data) - self.data = Var(shape=data.shape, init=data) - self.ref_port = RefPort(shape=data.shape) - - -# A minimal Process with a Var and a VarPort -class VarPortProcess(AbstractProcess): - def __init__(self, data: np.ndarray) -> None: - super().__init__(data=data) - self.data = Var(shape=data.shape, init=data) - self.var_port = VarPort(self.data) - - # A minimal PyProcModel implementing OutPortProcess @implements(proc=OutPortProcess, protocol=LoihiProtocol) @requires(CPU) @@ -717,47 +298,6 @@ def run_spk(self): print("Received input data for InPortProcess: ", str(self.data)) -# A minimal PyProcModel implementing RefPortWriteProcess -@implements(proc=RefPortWriteProcess, protocol=LoihiProtocol) -@requires(CPU) -@tag('floating_pt') -class PyRefPortWriteProcessModelFloat(PyLoihiProcessModel): - ref_port: PyRefPort = LavaPyType(PyRefPort.VEC_DENSE, np.int32) - data: np.ndarray = LavaPyType(np.ndarray, np.int32) - - def pre_guard(self): - return True - - def run_pre_mgmt(self): - self.ref_port.write(self.data) - print("Sent output data of RefPortWriteProcess: ", str(self.data)) - - -# A minimal PyProcModel implementing RefPortReadProcess -@implements(proc=RefPortReadProcess, protocol=LoihiProtocol) -@requires(CPU) -@tag('floating_pt') -class PyRefPortReadProcessModelFloat(PyLoihiProcessModel): - ref_port: PyRefPort = LavaPyType(PyRefPort.VEC_DENSE, np.int32) - data: np.ndarray = LavaPyType(np.ndarray, np.int32) - - def pre_guard(self): - return True - - def run_pre_mgmt(self): - self.data = self.ref_port.read() - print("Received input data for RefPortReadProcess: ", str(self.data)) - - -# A minimal PyProcModel implementing VarPortProcess -@implements(proc=VarPortProcess, protocol=LoihiProtocol) -@requires(CPU) -@tag('floating_pt') -class PyVarPortProcessModelFloat(PyLoihiProcessModel): - var_port: PyInPort = LavaPyType(PyVarPort.VEC_DENSE, np.int32) - data: np.ndarray = LavaPyType(np.ndarray, np.int32) - - # A minimal hierarchical ProcModel with a nested OutPortProcess @implements(proc=HOutPortProcess) class SubHOutPortProcModel(AbstractSubProcessModel): From abf03bd6d6dfff49969b7f83f989949c65e38e09 Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Tue, 22 Feb 2022 23:03:13 +0100 Subject: [PATCH 13/21] Fixed linter issues Signed-off-by: Mathis Richter --- src/lava/magma/core/process/ports/ports.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lava/magma/core/process/ports/ports.py b/src/lava/magma/core/process/ports/ports.py index bd505dc9c..56d2ffed6 100644 --- a/src/lava/magma/core/process/ports/ports.py +++ b/src/lava/magma/core/process/ports/ports.py @@ -197,8 +197,8 @@ def reshape(self, new_shape: ty.Tuple) -> "ReshapePort": not change. """ # TODO (MR): Implement for other types of Ports - if not (isinstance(self, OutPort) or - isinstance(self, AbstractVirtualPort)): + if not (isinstance(self, OutPort) + or isinstance(self, AbstractVirtualPort)): raise NotImplementedError("reshape/flatten are only implemented " "for OutPorts") @@ -256,8 +256,8 @@ def transpose( of dimensions must not change. """ # TODO (MR): Implement for other types of Ports - if not (isinstance(self, OutPort) or - isinstance(self, AbstractVirtualPort)): + if not (isinstance(self, OutPort) + or isinstance(self, AbstractVirtualPort)): raise NotImplementedError("transpose is only implemented for " "OutPorts") From 6ca8a516e178d81b48d91b1a45f2780054bed4a9 Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Fri, 25 Feb 2022 13:33:50 +0100 Subject: [PATCH 14/21] Raising an exception when executing ConcatPort Signed-off-by: Mathis Richter --- src/lava/magma/core/process/ports/ports.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lava/magma/core/process/ports/ports.py b/src/lava/magma/core/process/ports/ports.py index 56d2ffed6..dfa82c301 100644 --- a/src/lava/magma/core/process/ports/ports.py +++ b/src/lava/magma/core/process/ports/ports.py @@ -163,12 +163,18 @@ def get_incoming_virtual_ports(self) -> ty.List["AbstractVirtualPort"]: for p in self.in_connections: virtual_ports += p.get_incoming_virtual_ports() if isinstance(p, AbstractVirtualPort): + # TODO (MR): ConcatPorts are not yet supported by the + # compiler - until then, an exception is raised. + if isinstance(p, ConcatPort): + raise NotImplementedError("ConcatPorts are not yet " + "supported.") + virtual_ports.append(p) num_virtual_ports += 1 if num_virtual_ports > 1: - raise NotImplementedError("joining multiple virtual ports is " - "not yet supported") + raise NotImplementedError("Joining multiple virtual ports is " + "not yet supported.") return virtual_ports From 0d5b0bf33761f8875350ec48f267e744dc0c8b7e Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Sun, 27 Feb 2022 22:33:42 +0100 Subject: [PATCH 15/21] Unit tests for virtual ports between OutPorts and InPorts in hierarchical Processes. Signed-off-by: Mathis Richter --- .../ports/test_virtual_ports_in_process.py | 166 +++++++++++++++--- 1 file changed, 139 insertions(+), 27 deletions(-) diff --git a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py index d415e8e23..43c886d14 100644 --- a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py +++ b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py @@ -51,7 +51,7 @@ def setUp(self) -> None: self.new_shape = (12, 2) self.input_data = np.random.randint(256, size=self.shape) - def test_virtual_ports_between_hierarchical_processes(self) -> None: + def test_outport_to_inport_in_hierarchical_processes(self) -> None: """Tests a virtual port between an OutPort of a hierarchical Process and an InPort of another hierarchical Process.""" @@ -78,6 +78,63 @@ def test_virtual_ports_between_hierarchical_processes(self) -> None: f'{expected[output!=expected] =}\n' ) + def test_inport_to_inport_in_a_hierarchical_process(self) -> None: + """Tests a virtual port between an InPort of a hierarchical Process + and an InPort in a nested Process. + + The data comes from an OutPortProcess and enters the hierarchical + Process (HVPInPortProcess) via its InPort. That InPort is connected + via a virtual port to the InPort of the nested Process. The data is + from there written into a Var 'data' of the nested Process, for which + the Var 's_data' of the hierarchical Process is an alias. + """ + + out_port_process = OutPortProcess(data=self.input_data) + h_proc = HVPInPortProcess(h_shape=self.shape, s_shape=self.new_shape) + + out_port_process.out_port.connect(h_proc.in_port) + + h_proc.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = h_proc.s_data.get() + h_proc.stop() + + expected = self.input_data.reshape(self.new_shape) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_outport_to_outport_in_a_hierarchical_process(self) -> None: + """Tests a virtual port between an OutPort of a child Process and an + OutPort of the corresponding hierarchical parent Process. + + The data comes from the child Process, is passed from its OutPort + through a virtual port (where it is reshaped) to the OutPort of the + hierarchical Process (HVPOutPortProcess). From there it is passed to + the InPort of the InPortProcess and written into a Var 'data'. + """ + + h_proc = HVPOutPortProcess(h_shape=self.new_shape, data=self.input_data) + in_port_process = InPortProcess(shape=self.new_shape) + + h_proc.out_port.connect(in_port_process.in_port) + + h_proc.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = in_port_process.data.get() + h_proc.stop() + + expected = self.input_data.reshape(self.new_shape) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + def test_chaining_multiple_virtual_ports(self) -> None: """Tests whether two virtual ReshapePorts can be chained through the flatten() method.""" @@ -246,32 +303,6 @@ def __init__(self, data: np.ndarray) -> None: self.out_port = OutPort(shape=data.shape) -# A minimal Process with an InPort -class InPortProcess(AbstractProcess): - def __init__(self, shape: ty.Tuple[int, ...]) -> None: - super().__init__(shape=shape) - self.data = Var(shape=shape, init=np.zeros(shape)) - self.in_port = InPort(shape=shape) - - -# A minimal hierarchical Process with an OutPort -class HOutPortProcess(AbstractProcess): - def __init__(self, data: np.ndarray) -> None: - super().__init__(data=data) - self.data = Var(shape=data.shape, init=data) - self.out_port = OutPort(shape=data.shape) - self.proc_params['data'] = data - - -# A minimal hierarchical Process with an InPort and a Var -class HInPortProcess(AbstractProcess): - def __init__(self, shape: ty.Tuple[int, ...]) -> None: - super().__init__(shape=shape) - self.data = Var(shape=shape, init=np.zeros(shape)) - self.in_port = InPort(shape=shape) - self.proc_params['shape'] = shape - - # A minimal PyProcModel implementing OutPortProcess @implements(proc=OutPortProcess, protocol=LoihiProtocol) @requires(CPU) @@ -285,6 +316,14 @@ def run_spk(self): self.log.info("Sent output data of OutPortProcess: ", str(self.data)) +# A minimal Process with an InPort +class InPortProcess(AbstractProcess): + def __init__(self, shape: ty.Tuple[int, ...]) -> None: + super().__init__(shape=shape) + self.data = Var(shape=shape, init=np.zeros(shape)) + self.in_port = InPort(shape=shape) + + # A minimal PyProcModel implementing InPortProcess @implements(proc=InPortProcess, protocol=LoihiProtocol) @requires(CPU) @@ -298,6 +337,14 @@ def run_spk(self): self.log.info("Received input data for InPortProcess: ", str(self.data)) +# A minimal hierarchical Process with an OutPort +class HOutPortProcess(AbstractProcess): + def __init__(self, data: np.ndarray) -> None: + super().__init__(data=data) + self.out_port = OutPort(shape=data.shape) + self.proc_params['data'] = data + + # A minimal hierarchical ProcModel with a nested OutPortProcess @implements(proc=HOutPortProcess) class SubHOutPortProcModel(AbstractSubProcessModel): @@ -306,6 +353,15 @@ def __init__(self, proc): self.out_proc.out_port.connect(proc.out_port) +# A minimal hierarchical Process with an InPort and a Var +class HInPortProcess(AbstractProcess): + def __init__(self, shape: ty.Tuple[int, ...]) -> None: + super().__init__(shape=shape) + self.data = Var(shape=shape, init=np.zeros(shape)) + self.in_port = InPort(shape=shape) + self.proc_params['shape'] = shape + + # A minimal hierarchical ProcModel with a nested InPortProcess and an aliased # Var @implements(proc=HInPortProcess) @@ -316,5 +372,61 @@ def __init__(self, proc): proc.data.alias(self.in_proc.data) +# A minimal hierarchical Process with an InPort, where the SubProcessModel +# connects the InPort to another InPort via a virtual port. The data that +# comes in through the InPort will eventually be accessible through the +# 's_data' Var. +class HVPInPortProcess(AbstractProcess): + def __init__(self, + h_shape: ty.Tuple[int, ...], + s_shape: ty.Tuple[int, ...]) -> None: + super().__init__() + self.s_data = Var(shape=s_shape, init=np.zeros(s_shape)) + self.in_port = InPort(shape=h_shape) + self.proc_params['s_shape'] = s_shape + + +# A minimal hierarchical ProcModel with a nested InPortProcess and an aliased +# Var +@implements(proc=HVPInPortProcess) +class SubHVPInPortProcModel(AbstractSubProcessModel): + def __init__(self, proc): + self.in_proc = InPortProcess(shape=proc.proc_params['s_shape']) + + virtual_port = MockVirtualPort(new_shape=proc.proc_params['s_shape']) + proc.in_port._connect_forward( + [virtual_port], AbstractPort, assert_same_shape=False + ) + virtual_port.connect(self.in_proc.in_port) + + proc.s_data.alias(self.in_proc.data) + + +# A minimal hierarchical Process with an OutPort, where the data that is +# given as an argument may have a different shape than the OutPort of the +# Process. +class HVPOutPortProcess(AbstractProcess): + def __init__(self, + h_shape: ty.Tuple[int, ...], + data: np.ndarray) -> None: + super().__init__(h_shape=h_shape, data=data) + self.out_port = OutPort(shape=h_shape) + self.proc_params['data'] = data + self.proc_params['h_shape'] = h_shape + + +# A minimal hierarchical ProcModel with a nested OutPortProcess +@implements(proc=HVPOutPortProcess) +class SubHVPOutPortProcModel(AbstractSubProcessModel): + def __init__(self, proc): + self.out_proc = OutPortProcess(data=proc.proc_params['data']) + + virtual_port = MockVirtualPort(new_shape=proc.proc_params['h_shape']) + self.out_proc.out_port._connect_forward( + [virtual_port], AbstractPort, assert_same_shape=False + ) + virtual_port.connect(proc.out_port) + + if __name__ == '__main__': unittest.main() From 6a156661f6ea414865876e6dcefcd0d6919523cc Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Mon, 28 Feb 2022 11:38:58 +0100 Subject: [PATCH 16/21] RefPort writing to an explicit VarPort via a virtual port. Signed-off-by: Mathis Richter --- src/lava/magma/compiler/builders/builder.py | 3 +- src/lava/magma/compiler/compiler.py | 6 +- src/lava/magma/compiler/utils.py | 1 + src/lava/magma/core/model/py/ports.py | 26 +++++- .../ports/test_virtual_ports_in_process.py | 79 ++++++++++++++++++- 5 files changed, 109 insertions(+), 6 deletions(-) diff --git a/src/lava/magma/compiler/builders/builder.py b/src/lava/magma/compiler/builders/builder.py index c9aa45fc7..0a5fef510 100644 --- a/src/lava/magma/compiler/builders/builder.py +++ b/src/lava/magma/compiler/builders/builder.py @@ -422,7 +422,8 @@ def build(self): csp_send = csp_ports[0] if isinstance( csp_ports[0], CspSendPort) else csp_ports[1] port = port_cls( - p.var_name, csp_send, csp_recv, pm, p.shape, p.d_type) + p.var_name, csp_send, csp_recv, pm, p.shape, p.d_type, + p.transform_funcs) # Create dynamic VarPort attribute on ProcModel setattr(pm, name, port) diff --git a/src/lava/magma/compiler/compiler.py b/src/lava/magma/compiler/compiler.py index 14a513e8e..7bf2480a6 100644 --- a/src/lava/magma/compiler/compiler.py +++ b/src/lava/magma/compiler/compiler.py @@ -377,6 +377,9 @@ def _compile_proc_models( # Create VarPortInitializers (contain also the Var name) var_ports = [] for pt in list(p.var_ports): + transform_funcs = \ + [vp.get_transform_func() + for vp in pt.get_incoming_virtual_ports()] var_ports.append( VarPortInitializer( pt.name, @@ -385,7 +388,8 @@ def _compile_proc_models( self._get_port_dtype(pt, pm), pt.__class__.__name__, pp_ch_size, - self._map_var_port_class(pt, proc_groups))) + self._map_var_port_class(pt, proc_groups), + transform_funcs)) # Set implicit VarPorts (created by connecting a RefPort # directly to a Var) as attribute to ProcessModel diff --git a/src/lava/magma/compiler/utils.py b/src/lava/magma/compiler/utils.py index 27f4775fb..3787b274e 100644 --- a/src/lava/magma/compiler/utils.py +++ b/src/lava/magma/compiler/utils.py @@ -31,3 +31,4 @@ class VarPortInitializer: port_type: str size: int port_cls: type + transform_funcs: ty.List[ft.partial] = None diff --git a/src/lava/magma/core/model/py/ports.py b/src/lava/magma/core/model/py/ports.py index 6a3c6ffa2..3f2642dc9 100644 --- a/src/lava/magma/core/model/py/ports.py +++ b/src/lava/magma/core/model/py/ports.py @@ -660,7 +660,10 @@ def __init__(self, csp_recv_port: ty.Optional[CspRecvPort], process_model: AbstractProcessModel, shape: ty.Tuple[int, ...] = tuple(), - d_type: type = int): + d_type: type = int, + transform_funcs: ty.Optional[ty.List[ft.partial]] = None): + + self._transform_funcs = transform_funcs self._csp_recv_port = csp_recv_port self._csp_send_port = csp_send_port self.var_name = var_name @@ -692,6 +695,25 @@ def service(self): """ pass + def _transform(self, data: np.array) -> np.array: + """Applies all transformation function pointers to the input data. + + Parameters + ---------- + data : numpy.ndarray + data sent on the port that shall be transformed + + Returns + ------- + data : numpy.ndarray + data, transformed by the outgoing virtual ports + """ + if self._transform_funcs: + # apply all transformation functions to the outgoing data + for f in self._transform_funcs: + data = f(data) + return data + class PyVarPortVectorDense(PyVarPort): """Python implementation of VarPort for dense vector data.""" @@ -712,7 +734,7 @@ def service(self): # Set the value of the Var with the given data if enum_equal(cmd, VarPortCmd.SET): - data = self._csp_recv_port.recv() + data = self._transform(self._csp_recv_port.recv()) setattr(self._process_model, self.var_name, data) elif enum_equal(cmd, VarPortCmd.GET): data = getattr(self._process_model, self.var_name) diff --git a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py index 43c886d14..398036a64 100644 --- a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py +++ b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py @@ -19,13 +19,17 @@ from lava.magma.core.run_configs import Loihi1SimCfg from lava.magma.core.model.py.ports import ( PyInPort, - PyOutPort + PyOutPort, + PyRefPort, + PyVarPort ) from lava.magma.core.process.ports.ports import ( AbstractPort, AbstractVirtualPort, InPort, - OutPort + OutPort, + RefPort, + VarPort ) @@ -135,6 +139,35 @@ def test_outport_to_outport_in_a_hierarchical_process(self) -> None: f'{expected[output!=expected] =}\n' ) + def test_refport_write_to_varport(self) -> None: + """Tests a virtual port between a RefPort and a VarPort, + where the RefPort writes to the VarPort.""" + + source = RefPortWriteProcess(data=self.input_data) + sink = VarPortProcess(data=np.zeros(self.new_shape)) + + virtual_port = MockVirtualPort(new_shape=self.new_shape) + + source.ref_port._connect_forward( + [virtual_port], AbstractPort, assert_same_shape=False + ) + virtual_port.connect(sink.var_port) + + try: + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + finally: + sink.stop() + + expected = self.input_data.reshape(self.new_shape) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + def test_chaining_multiple_virtual_ports(self) -> None: """Tests whether two virtual ReshapePorts can be chained through the flatten() method.""" @@ -428,5 +461,47 @@ def __init__(self, proc): virtual_port.connect(proc.out_port) +# A minimal Process with a RefPort that writes +class RefPortWriteProcess(AbstractProcess): + def __init__(self, data: np.ndarray) -> None: + super().__init__(data=data) + self.data = Var(shape=data.shape, init=data) + self.ref_port = RefPort(shape=data.shape) + + +# A minimal PyProcModel implementing RefPortWriteProcess +@implements(proc=RefPortWriteProcess, protocol=LoihiProtocol) +@requires(CPU) +@tag('floating_pt') +class PyRefPortWriteProcessModelFloat(PyLoihiProcessModel): + ref_port: PyRefPort = LavaPyType(PyRefPort.VEC_DENSE, np.int32) + data: np.ndarray = LavaPyType(np.ndarray, np.int32) + + def post_guard(self): + return True + + def run_post_mgmt(self): + self.ref_port.write(self.data) + self.log.info("Sent output data of RefPortWriteProcess: ", + str(self.data)) + + +# A minimal Process with a Var and a VarPort +class VarPortProcess(AbstractProcess): + def __init__(self, data: np.ndarray) -> None: + super().__init__(data=data) + self.data = Var(shape=data.shape, init=data) + self.var_port = VarPort(self.data) + + +# A minimal PyProcModel implementing VarPortProcess +@implements(proc=VarPortProcess, protocol=LoihiProtocol) +@requires(CPU) +@tag('floating_pt') +class PyVarPortProcessModelFloat(PyLoihiProcessModel): + var_port: PyInPort = LavaPyType(PyVarPort.VEC_DENSE, np.int32) + data: np.ndarray = LavaPyType(np.ndarray, np.int32) + + if __name__ == '__main__': unittest.main() From 9ceddb1dd2c532a837044f236e6b8c54e23fbe26 Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Mon, 28 Feb 2022 15:33:41 +0100 Subject: [PATCH 17/21] RefPort reading from an explicit VarPort via a virtual port. Signed-off-by: Mathis Richter --- src/lava/magma/compiler/builders/builder.py | 3 +- src/lava/magma/compiler/compiler.py | 35 ++- src/lava/magma/core/model/py/model.py | 2 +- src/lava/magma/core/model/py/ports.py | 44 +++- src/lava/magma/core/process/ports/ports.py | 83 +++++-- .../ports/test_virtual_ports_in_process.py | 226 +++++++++++++++--- 6 files changed, 311 insertions(+), 82 deletions(-) diff --git a/src/lava/magma/compiler/builders/builder.py b/src/lava/magma/compiler/builders/builder.py index 0a5fef510..17f15f21d 100644 --- a/src/lava/magma/compiler/builders/builder.py +++ b/src/lava/magma/compiler/builders/builder.py @@ -401,7 +401,8 @@ def build(self): csp_send = csp_ports[0] if isinstance( csp_ports[0], CspSendPort) else csp_ports[1] - port = port_cls(csp_send, csp_recv, pm, p.shape, lt.d_type) + port = port_cls(csp_send, csp_recv, pm, p.shape, lt.d_type, + p.transform_funcs) # Create dynamic RefPort attribute on ProcModel setattr(pm, name, port) diff --git a/src/lava/magma/compiler/compiler.py b/src/lava/magma/compiler/compiler.py index 7bf2480a6..1c5932896 100644 --- a/src/lava/magma/compiler/compiler.py +++ b/src/lava/magma/compiler/compiler.py @@ -352,13 +352,14 @@ def _compile_proc_models( for pt in (list(p.in_ports) + list(p.out_ports)): # For all InPorts that receive input from # virtual ports... - transform_funcs = None + transform_funcs = [] if isinstance(pt, InPort): # ... extract a function pointer to the # transformation function of each virtual port. transform_funcs = \ - [vp.get_transform_func() + [vp.get_transform_func_fwd() for vp in pt.get_incoming_virtual_ports()] + pi = PortInitializer(pt.name, pt.shape, self._get_port_dtype(pt, pm), @@ -366,22 +367,29 @@ def _compile_proc_models( pp_ch_size, transform_funcs) ports.append(pi) + # Create RefPort (also use PortInitializers) - ref_ports = list(p.ref_ports) - ref_ports = [ - PortInitializer(pt.name, - pt.shape, - self._get_port_dtype(pt, pm), - pt.__class__.__name__, - pp_ch_size) for pt in ref_ports] + ref_ports = [] + for pt in list(p.ref_ports): + transform_funcs = \ + [vp.get_transform_func_bwd() + for vp in pt.get_outgoing_virtual_ports()] + + pi = PortInitializer(pt.name, + pt.shape, + self._get_port_dtype(pt, pm), + pt.__class__.__name__, + pp_ch_size, + transform_funcs) + ref_ports.append(pi) + # Create VarPortInitializers (contain also the Var name) var_ports = [] for pt in list(p.var_ports): transform_funcs = \ - [vp.get_transform_func() + [vp.get_transform_func_fwd() for vp in pt.get_incoming_virtual_ports()] - var_ports.append( - VarPortInitializer( + pi = VarPortInitializer( pt.name, pt.shape, pt.var.name, @@ -389,7 +397,8 @@ def _compile_proc_models( pt.__class__.__name__, pp_ch_size, self._map_var_port_class(pt, proc_groups), - transform_funcs)) + transform_funcs) + var_ports.append(pi) # Set implicit VarPorts (created by connecting a RefPort # directly to a Var) as attribute to ProcessModel diff --git a/src/lava/magma/core/model/py/model.py b/src/lava/magma/core/model/py/model.py index a9bc34560..c93e91c7b 100644 --- a/src/lava/magma/core/model/py/model.py +++ b/src/lava/magma/core/model/py/model.py @@ -114,7 +114,7 @@ def _get_var(self): data_port.send(enum_to_np(var)) elif isinstance(var, np.ndarray): # FIXME: send a whole vector (also runtime_service.py) - var_iter = np.nditer(var) + var_iter = np.nditer(var, order='C') num_items: np.integer = np.prod(var.shape) data_port.send(enum_to_np(num_items)) for value in var_iter: diff --git a/src/lava/magma/core/model/py/ports.py b/src/lava/magma/core/model/py/ports.py index 3f2642dc9..b96bc77a7 100644 --- a/src/lava/magma/core/model/py/ports.py +++ b/src/lava/magma/core/model/py/ports.py @@ -460,7 +460,10 @@ def __init__(self, csp_recv_port: ty.Optional[CspRecvPort], process_model: AbstractProcessModel, shape: ty.Tuple[int, ...] = tuple(), - d_type: type = int): + d_type: type = int, + transform_funcs: ty.Optional[ty.List[ft.partial]] = None): + + self._transform_funcs = transform_funcs self._csp_recv_port = csp_recv_port self._csp_send_port = csp_send_port super().__init__(process_model, shape, d_type) @@ -513,6 +516,25 @@ def write( """ pass + def _transform(self, recv_data: np.array) -> np.array: + """Applies all transformation function pointers to the input data. + + Parameters + ---------- + recv_data : numpy.ndarray + data received on the port that shall be transformed + + Returns + ------- + recv_data : numpy.ndarray + received data, transformed by the incoming virtual ports + """ + if self._transform_funcs: + # apply all transformation functions to the received data + for f in reversed(self._transform_funcs): + recv_data = f(recv_data) + return recv_data + class PyRefPortVectorDense(PyRefPort): """Python implementation of RefPort for dense vector data.""" @@ -529,8 +551,10 @@ def read(self) -> np.ndarray: header = np.ones(self._csp_send_port.shape) * VarPortCmd.GET self._csp_send_port.send(header) - return self._csp_recv_port.recv() + return self._transform(self._csp_recv_port.recv()) + # TODO (MR): self._shape must be set to the correct shape when + # instantiating the Port return np.zeros(self._shape, self._d_type) def write(self, data: np.ndarray): @@ -695,24 +719,24 @@ def service(self): """ pass - def _transform(self, data: np.array) -> np.array: + def _transform(self, recv_data: np.array) -> np.array: """Applies all transformation function pointers to the input data. Parameters ---------- - data : numpy.ndarray - data sent on the port that shall be transformed + recv_data : numpy.ndarray + data received on the port that shall be transformed Returns ------- - data : numpy.ndarray - data, transformed by the outgoing virtual ports + recv_data : numpy.ndarray + received data, transformed by the incoming virtual ports """ if self._transform_funcs: - # apply all transformation functions to the outgoing data + # apply all transformation functions to the received data for f in self._transform_funcs: - data = f(data) - return data + recv_data = f(recv_data) + return recv_data class PyVarPortVectorDense(PyVarPort): diff --git a/src/lava/magma/core/process/ports/ports.py b/src/lava/magma/core/process/ports/ports.py index dfa82c301..83a0d2a0d 100644 --- a/src/lava/magma/core/process/ports/ports.py +++ b/src/lava/magma/core/process/ports/ports.py @@ -178,6 +178,39 @@ def get_incoming_virtual_ports(self) -> ty.List["AbstractVirtualPort"]: return virtual_ports + def get_outgoing_virtual_ports(self) -> ty.List["AbstractVirtualPort"]: + """Returns the list of all outgoing virtual ports in order from + the current port to the destination port. + + Returns + ------- + virtual_ports : list(AbstractVirtualPorts) + the list of all outgoing virtual ports, sorted from source to + destination port + """ + if len(self.out_connections) == 0: + return [] + else: + virtual_ports = [] + num_virtual_ports = 0 + for p in self.out_connections: + virtual_ports += p.get_outgoing_virtual_ports() + if isinstance(p, AbstractVirtualPort): + # TODO (MR): ConcatPorts are not yet supported by the + # compiler - until then, an exception is raised. + if isinstance(p, ConcatPort): + raise NotImplementedError("ConcatPorts are not yet " + "supported.") + + virtual_ports.append(p) + num_virtual_ports += 1 + + if num_virtual_ports > 1: + raise NotImplementedError("Forking a virtual port is " + "not yet supported.") + + return virtual_ports + def get_dst_ports(self, _include_self=False) -> ty.List["AbstractPort"]: """Returns the list of all destination ports that this port connects to either directly or indirectly (through other ports).""" @@ -192,7 +225,7 @@ def get_dst_ports(self, _include_self=False) -> ty.List["AbstractPort"]: ports += p.get_dst_ports(True) return ports - def reshape(self, new_shape: ty.Tuple) -> "ReshapePort": + def reshape(self, new_shape: ty.Tuple[int, ...]) -> "ReshapePort": """Reshapes this port by deriving and returning a new virtual ReshapePort with the new shape. This implies that the resulting ReshapePort can only be forward connected to another port. @@ -202,16 +235,10 @@ def reshape(self, new_shape: ty.Tuple) -> "ReshapePort": :param new_shape: New shape of port. Number of total elements must not change. """ - # TODO (MR): Implement for other types of Ports - if not (isinstance(self, OutPort) - or isinstance(self, AbstractVirtualPort)): - raise NotImplementedError("reshape/flatten are only implemented " - "for OutPorts") - if self.size != math.prod(new_shape): raise pe.ReshapeError(self.shape, new_shape) - reshape_port = ReshapePort(new_shape) + reshape_port = ReshapePort(new_shape, old_shape=self.shape) self._connect_forward( [reshape_port], AbstractPort, assert_same_shape=False ) @@ -249,7 +276,8 @@ def concat_with( def transpose( self, - axes: ty.Optional[ty.Union[ty.Tuple, ty.List]] = None + axes: ty.Optional[ty.Union[ty.Tuple[int, ...], + ty.List]] = None ) -> "TransposePort": """Permutes the tensor dimension of this port by deriving and returning a new virtual TransposePort the new permuted dimension. This implies @@ -261,12 +289,6 @@ def transpose( :param axes: Order of permutation. Number of total elements and number of dimensions must not change. """ - # TODO (MR): Implement for other types of Ports - if not (isinstance(self, OutPort) - or isinstance(self, AbstractVirtualPort)): - raise NotImplementedError("transpose is only implemented for " - "OutPorts") - if axes is None: axes = tuple(reversed(range(len(self.shape)))) else: @@ -693,7 +715,11 @@ def connect(self, ports: ty.Union["AbstractPort", ty.List["AbstractPort"]]): self._connect_forward(to_list(ports), port_type) @abstractmethod - def get_transform_func(self) -> ft.partial: + def get_transform_func_fwd(self) -> ft.partial: + pass + + @abstractmethod + def get_transform_func_bwd(self) -> ft.partial: pass @@ -703,12 +729,18 @@ class ReshapePort(AbstractVirtualPort): It is used by the compiler to map the indices of the underlying tensor-valued data array from the derived to the new shape.""" - def __init__(self, new_shape: ty.Tuple): + def __init__(self, + new_shape: ty.Tuple[int, ...], + old_shape: ty.Tuple[int, ...]): AbstractPort.__init__(self, new_shape) + self.old_shape = old_shape - def get_transform_func(self) -> ft.partial: + def get_transform_func_fwd(self) -> ft.partial: return ft.partial(np.reshape, newshape=self.shape) + def get_transform_func_bwd(self) -> ft.partial: + return ft.partial(np.reshape, newshape=self.old_shape) + class ConcatPort(AbstractVirtualPort): """A ConcatPort is a virtual port that allows to concatenate multiple @@ -751,7 +783,11 @@ def _get_new_shape(ports: ty.List[AbstractPort], axis): new_shape = shapes_ex_axis[0] return new_shape[:axis] + (total_size,) + new_shape[axis:] - def get_transform_func(self) -> ft.partial: + def get_transform_func_fwd(self) -> ft.partial: + # TODO (MR): not yet implemented + raise NotImplementedError() + + def get_transform_func_bwd(self) -> ft.partial: # TODO (MR): not yet implemented raise NotImplementedError() @@ -771,11 +807,14 @@ class TransposePort(AbstractVirtualPort): def __init__(self, new_shape: ty.Tuple[int, ...], axes: ty.Tuple[int, ...]): - self._axes = axes + self.axes = axes AbstractPort.__init__(self, new_shape) - def get_transform_func(self) -> ft.partial: - return ft.partial(np.transpose, axes=self._axes) + def get_transform_func_fwd(self) -> ft.partial: + return ft.partial(np.transpose, axes=self.axes) + + def get_transform_func_bwd(self) -> ft.partial: + return ft.partial(np.transpose, axes=np.argsort(self.axes)) # ToDo: TBD... diff --git a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py index 398036a64..ac65c4c34 100644 --- a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py +++ b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py @@ -38,12 +38,18 @@ class MockVirtualPort(AbstractVirtualPort, AbstractPort): """A mock-up of a virtual port that reshapes the input.""" - def __init__(self, new_shape: ty.Tuple): + def __init__(self, + new_shape: ty.Tuple[int, ...], + old_shape: ty.Optional[ty.Tuple[int, ...]] = None): AbstractPort.__init__(self, new_shape) + self.old_shape = old_shape - def get_transform_func(self) -> ft.partial: + def get_transform_func_fwd(self) -> ft.partial: return ft.partial(np.reshape, newshape=self.shape) + def get_transform_func_bwd(self) -> ft.partial: + return ft.partial(np.reshape, newshape=self.old_shape) + class TestVirtualPortNetworkTopologies(unittest.TestCase): """Tests different network topologies that include virtual ports using a @@ -139,35 +145,6 @@ def test_outport_to_outport_in_a_hierarchical_process(self) -> None: f'{expected[output!=expected] =}\n' ) - def test_refport_write_to_varport(self) -> None: - """Tests a virtual port between a RefPort and a VarPort, - where the RefPort writes to the VarPort.""" - - source = RefPortWriteProcess(data=self.input_data) - sink = VarPortProcess(data=np.zeros(self.new_shape)) - - virtual_port = MockVirtualPort(new_shape=self.new_shape) - - source.ref_port._connect_forward( - [virtual_port], AbstractPort, assert_same_shape=False - ) - virtual_port.connect(sink.var_port) - - try: - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - finally: - sink.stop() - - expected = self.input_data.reshape(self.new_shape) - self.assertTrue( - np.all(output == expected), - f'Input and output do not match.\n' - f'{output[output!=expected]=}\n' - f'{expected[output!=expected] =}\n' - ) - def test_chaining_multiple_virtual_ports(self) -> None: """Tests whether two virtual ReshapePorts can be chained through the flatten() method.""" @@ -231,10 +208,7 @@ class TestTransposePort(unittest.TestCase): def setUp(self) -> None: self.num_steps = 1 self.axes = (2, 0, 1) - self.axes_reverse = list(self.axes) - for idx, ax in enumerate(self.axes): - self.axes_reverse[ax] = idx - self.axes_reverse = tuple(self.axes_reverse) + self.axes_reverse = np.argsort(self.axes) self.shape = (4, 3, 2) self.shape_transposed = tuple(self.shape[i] for i in self.axes) self.shape_transposed_reverse = \ @@ -262,6 +236,61 @@ def test_transpose_outport_to_inport(self) -> None: f'{expected[output!=expected] =}\n' ) + def test_transpose_refport_write_to_varport(self) -> None: + """Tests a virtual TransposePort between a RefPort and a VarPort, + where the RefPort writes to the VarPort.""" + + source = RefPortWriteProcess(data=self.input_data) + sink = VarPortProcess(data=np.zeros(self.shape_transposed)) + + source.ref_port.transpose(axes=self.axes).connect(sink.var_port) + + try: + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + finally: + sink.stop() + + expected = self.input_data.transpose(self.axes) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_transpose_refport_read_from_varport(self) -> None: + """Tests a virtual TransposePort between a RefPort and a VarPort, + where the RefPort reads from the VarPort.""" + + source = RefPortReadProcess(data=np.zeros(self.shape)) + sink = VarPortProcess( + data=self.input_data.reshape(self.shape_transposed)) + + virtual_port = MockVirtualPort(new_shape=self.shape_transposed, + old_shape=self.shape) + + source.ref_port._connect_forward( + [virtual_port], AbstractPort, assert_same_shape=False + ) + virtual_port.connect(sink.var_port) + + try: + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = source.data.get() + finally: + sink.stop() + + expected = self.input_data + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + class TestReshapePort(unittest.TestCase): """Tests virtual ReshapePorts on Processes that are executed.""" @@ -294,6 +323,60 @@ def test_reshape_outport_to_inport(self) -> None: f'{expected[output!=expected] =}\n' ) + def test_reshape_refport_write_to_varport(self) -> None: + """Tests a virtual ReshapePort between a RefPort and a VarPort, + where the RefPort writes to the VarPort.""" + + source = RefPortWriteProcess(data=self.input_data) + sink = VarPortProcess(data=np.zeros(self.shape_reshaped)) + + source.ref_port.reshape(self.shape_reshaped).connect(sink.var_port) + + try: + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + finally: + sink.stop() + + expected = self.input_data.reshape(self.shape_reshaped) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_reshape_refport_read_from_varport(self) -> None: + """Tests a virtual ReshapePort between a RefPort and a VarPort, + where the RefPort reads from the VarPort.""" + + source = RefPortReadProcess(data=np.zeros(self.shape)) + sink = VarPortProcess(data=self.input_data.reshape(self.shape_reshaped)) + + virtual_port = MockVirtualPort(new_shape=self.shape_reshaped, + old_shape=self.shape) + + source.ref_port._connect_forward( + [virtual_port], AbstractPort, assert_same_shape=False + ) + virtual_port.connect(sink.var_port) + + try: + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = source.data.get() + finally: + sink.stop() + + expected = self.input_data + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + class TestFlattenPort(unittest.TestCase): """Tests virtual ReshapePorts, created by the flatten() method, @@ -327,6 +410,54 @@ def test_flatten_outport_to_inport(self) -> None: f'{expected[output!=expected] =}\n' ) + def test_flatten_refport_write_to_varport(self) -> None: + """Tests a virtual ReshapePort with flatten() between a RefPort and a + VarPort, where the RefPort writes to the VarPort.""" + + source = RefPortWriteProcess(data=self.input_data) + sink = VarPortProcess(data=np.zeros(self.shape_reshaped)) + + source.ref_port.flatten().connect(sink.var_port) + + try: + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + finally: + sink.stop() + + expected = self.input_data.ravel() + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_flatten_refport_read_from_varport(self) -> None: + """Tests a virtual ReshapePort between a RefPort and a VarPort, + where the RefPort reads from the VarPort.""" + + source = RefPortReadProcess(data=np.zeros(self.shape)) + sink = VarPortProcess(data=self.input_data.reshape(self.shape_reshaped)) + + source.ref_port.flatten().connect(sink.var_port) + + try: + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = source.data.get() + finally: + sink.stop() + + expected = self.input_data + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + # A minimal Process with an OutPort class OutPortProcess(AbstractProcess): @@ -486,6 +617,31 @@ def run_post_mgmt(self): str(self.data)) +# A minimal Process with a RefPort that reads +class RefPortReadProcess(AbstractProcess): + def __init__(self, data: np.ndarray) -> None: + super().__init__(data=data) + self.data = Var(shape=data.shape, init=data) + self.ref_port = RefPort(shape=data.shape) + + +# A minimal PyProcModel implementing RefPortReadProcess +@implements(proc=RefPortReadProcess, protocol=LoihiProtocol) +@requires(CPU) +@tag('floating_pt') +class PyRefPortReadProcessModelFloat(PyLoihiProcessModel): + ref_port: PyRefPort = LavaPyType(PyRefPort.VEC_DENSE, np.int32) + data: np.ndarray = LavaPyType(np.ndarray, np.int32) + + def post_guard(self): + return True + + def run_post_mgmt(self): + self.data = self.ref_port.read() + self.log.info("Received input data for RefPortReadProcess: ", + str(self.data)) + + # A minimal Process with a Var and a VarPort class VarPortProcess(AbstractProcess): def __init__(self, data: np.ndarray) -> None: From fbf1f54145ab90ebee1d735e4784a5c94e0c970d Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Tue, 1 Mar 2022 19:18:33 +0100 Subject: [PATCH 18/21] Fixed linter error Signed-off-by: Mathis Richter --- src/lava/magma/compiler/compiler.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lava/magma/compiler/compiler.py b/src/lava/magma/compiler/compiler.py index 1c5932896..7e3ac47ef 100644 --- a/src/lava/magma/compiler/compiler.py +++ b/src/lava/magma/compiler/compiler.py @@ -390,14 +390,14 @@ def _compile_proc_models( [vp.get_transform_func_fwd() for vp in pt.get_incoming_virtual_ports()] pi = VarPortInitializer( - pt.name, - pt.shape, - pt.var.name, - self._get_port_dtype(pt, pm), - pt.__class__.__name__, - pp_ch_size, - self._map_var_port_class(pt, proc_groups), - transform_funcs) + pt.name, + pt.shape, + pt.var.name, + self._get_port_dtype(pt, pm), + pt.__class__.__name__, + pp_ch_size, + self._map_var_port_class(pt, proc_groups), + transform_funcs) var_ports.append(pi) # Set implicit VarPorts (created by connecting a RefPort From ae4debfe7334167c1be8913285973fd355cfbe14 Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Tue, 1 Mar 2022 20:57:55 +0100 Subject: [PATCH 19/21] Unit tests for virtual ports between RefPorts and VarPorts in hierarchical Processes. Signed-off-by: Mathis Richter --- .../ports/test_virtual_ports_in_process.py | 386 +++++++++++++++--- 1 file changed, 320 insertions(+), 66 deletions(-) diff --git a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py index ac65c4c34..ad838e062 100644 --- a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py +++ b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py @@ -40,15 +40,15 @@ class MockVirtualPort(AbstractVirtualPort, AbstractPort): """A mock-up of a virtual port that reshapes the input.""" def __init__(self, new_shape: ty.Tuple[int, ...], - old_shape: ty.Optional[ty.Tuple[int, ...]] = None): + axes: ty.Tuple[int, ...]): AbstractPort.__init__(self, new_shape) - self.old_shape = old_shape + self.axes = axes def get_transform_func_fwd(self) -> ft.partial: - return ft.partial(np.reshape, newshape=self.shape) + return ft.partial(np.transpose, axes=self.axes) def get_transform_func_bwd(self) -> ft.partial: - return ft.partial(np.reshape, newshape=self.old_shape) + return ft.partial(np.transpose, axes=np.argsort(self.axes)) class TestVirtualPortNetworkTopologies(unittest.TestCase): @@ -58,7 +58,8 @@ class TestVirtualPortNetworkTopologies(unittest.TestCase): def setUp(self) -> None: self.num_steps = 1 self.shape = (4, 3, 2) - self.new_shape = (12, 2) + self.new_shape = (2, 4, 3) + self.axes = (2, 0, 1) self.input_data = np.random.randint(256, size=self.shape) def test_outport_to_inport_in_hierarchical_processes(self) -> None: @@ -68,19 +69,22 @@ def test_outport_to_inport_in_hierarchical_processes(self) -> None: source = HOutPortProcess(data=self.input_data) sink = HInPortProcess(shape=self.new_shape) - virtual_port = MockVirtualPort(new_shape=self.new_shape) + virtual_port = MockVirtualPort(new_shape=self.new_shape, + axes=self.axes) source.out_port._connect_forward( [virtual_port], AbstractPort, assert_same_shape=False ) virtual_port.connect(sink.in_port) - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() + try: + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + finally: + sink.stop() - expected = self.input_data.reshape(self.new_shape) + expected = self.input_data.transpose(self.axes) self.assertTrue( np.all(output == expected), f'Input and output do not match.\n' @@ -100,16 +104,20 @@ def test_inport_to_inport_in_a_hierarchical_process(self) -> None: """ out_port_process = OutPortProcess(data=self.input_data) - h_proc = HVPInPortProcess(h_shape=self.shape, s_shape=self.new_shape) + h_proc = HVPInPortProcess(h_shape=self.shape, + s_shape=self.new_shape, + axes=self.axes) out_port_process.out_port.connect(h_proc.in_port) - h_proc.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = h_proc.s_data.get() - h_proc.stop() + try: + h_proc.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = h_proc.s_data.get() + finally: + h_proc.stop() - expected = self.input_data.reshape(self.new_shape) + expected = self.input_data.transpose(self.axes) self.assertTrue( np.all(output == expected), f'Input and output do not match.\n' @@ -127,17 +135,161 @@ def test_outport_to_outport_in_a_hierarchical_process(self) -> None: the InPort of the InPortProcess and written into a Var 'data'. """ - h_proc = HVPOutPortProcess(h_shape=self.new_shape, data=self.input_data) + h_proc = HVPOutPortProcess(h_shape=self.new_shape, + data=self.input_data, + axes=self.axes) in_port_process = InPortProcess(shape=self.new_shape) h_proc.out_port.connect(in_port_process.in_port) - h_proc.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = in_port_process.data.get() - h_proc.stop() + try: + h_proc.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = in_port_process.data.get() + finally: + h_proc.stop() + + expected = self.input_data.transpose(self.axes) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_refport_to_refport_write_in_a_hierarchical_process(self) -> None: + """Tests a virtual port between a RefPort of a child Process + and a RefPort of the corresponding hierarchical parent Process. + For this test, the nested RefPortWriteProcess writes data into the + VarPort. + + The data comes from the child Process, is passed from its RefPort + through a virtual port (where it is reshaped) to the RefPort of the + hierarchical Process (HVPRefPortWriteProcess). From there it is + passed to the VarPort of the VarPortProcess and written into a + Var 'data'. + """ + + h_proc = HVPRefPortWriteProcess(h_shape=self.new_shape, + data=self.input_data, + axes=self.axes) + var_port_process = VarPortProcess(data=np.zeros(self.new_shape)) + + h_proc.ref_port.connect(var_port_process.var_port) + + try: + h_proc.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = var_port_process.data.get() + finally: + h_proc.stop() + + expected = self.input_data.transpose(self.axes) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_refport_to_refport_read_in_a_hierarchical_process(self) -> None: + """Tests a virtual port between a RefPort of a child Process + and a RefPort of the corresponding hierarchical parent Process. + For this test, the nested RefPortReadProcess reads data from the + VarPort. + + The data comes from a Var 'data' in the VarPortProcess. From there it + passes through the RefPort of the hierarchical Process + (HVPRefPortReadProcess), then through a virtual port (where it is + reshaped) to the RefPort of the child Process (RefPortReadProcess), + where it is written into a Var. That Var is again an alias for the + Var of the parent Process. + """ - expected = self.input_data.reshape(self.new_shape) + h_proc = HVPRefPortReadProcess(h_shape=self.new_shape, + s_shape=self.shape, + axes=self.axes) + var_port_process = \ + VarPortProcess(data=self.input_data.transpose(self.axes)) + + h_proc.ref_port.connect(var_port_process.var_port) + + try: + h_proc.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = h_proc.s_data.get() + finally: + h_proc.stop() + + expected = self.input_data + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_varport_to_varport_write_in_a_hierarchical_process(self) -> None: + """Tests a virtual port between a VarPort of a child Process + and a VarPort of the corresponding hierarchical parent Process. + For this test, data is written into the hierarchical Process. + + The data comes from a RefPortWriteProcess, is passed + to the VarPort of a hierarchical Process (HVPVarPortProcess), + from there through a virtual port (where it is reshaped) to the + VarPort of the child Process (VarPortProcess). From there it is + written into a Var 'data', which is an alias for a Var 's_data' in + the parent Process. + """ + + ref_proc = RefPortWriteProcess(data=self.input_data) + h_proc = HVPVarPortProcess(h_shape=self.shape, + s_data=np.zeros(self.new_shape), + axes=self.axes) + + ref_proc.ref_port.connect(h_proc.var_port) + + try: + h_proc.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = h_proc.s_data.get() + finally: + h_proc.stop() + + expected = self.input_data.transpose(self.axes) + self.assertTrue( + np.all(output == expected), + f'Input and output do not match.\n' + f'{output[output!=expected]=}\n' + f'{expected[output!=expected] =}\n' + ) + + def test_varport_to_varport_read_in_a_hierarchical_process(self) -> None: + """Tests a virtual port between a VarPort of a child Process + and a VarPort of the corresponding hierarchical parent Process. + For this test, data is read from the hierarchical Process. + + The data comes from the child VarPortProcess, from there it passes + through its VarPort, through a virtual port (where it is reshaped), + to the VarPort of the parent Process (HVPVarPortProcess), + to a RefPortReadProcess, and from there written into a Var. + """ + + ref_proc = RefPortReadProcess(data=np.zeros(self.shape)) + h_proc = HVPVarPortProcess(h_shape=self.shape, + s_data=self.input_data.transpose(self.axes), + axes=self.axes) + + ref_proc.ref_port.connect(h_proc.var_port) + + try: + h_proc.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = ref_proc.data.get() + finally: + h_proc.stop() + + expected = self.input_data self.assertTrue( np.all(output == expected), f'Input and output do not match.\n' @@ -150,11 +302,12 @@ def test_chaining_multiple_virtual_ports(self) -> None: flatten() method.""" source = OutPortProcess(data=self.input_data) - shape_final = (int(np.prod(self.shape)),) - sink = InPortProcess(shape=shape_final) + sink = InPortProcess(shape=self.shape) - virtual_port1 = MockVirtualPort(new_shape=self.new_shape) - virtual_port2 = MockVirtualPort(new_shape=shape_final) + virtual_port1 = MockVirtualPort(new_shape=self.new_shape, + axes=self.axes) + virtual_port2 = MockVirtualPort(new_shape=self.shape, + axes=tuple(np.argsort(self.axes))) source.out_port._connect_forward( [virtual_port1], AbstractPort, assert_same_shape=False @@ -164,12 +317,14 @@ def test_chaining_multiple_virtual_ports(self) -> None: ) virtual_port2.connect(sink.in_port) - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() + try: + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + finally: + sink.stop() - expected = self.input_data.ravel() + expected = self.input_data self.assertTrue( np.all(output == expected), f'Input and output do not match.\n' @@ -184,8 +339,10 @@ def test_joining_virtual_ports_throws_exception(self) -> None: source2 = OutPortProcess(data=self.input_data) sink = InPortProcess(shape=self.new_shape) - virtual_port1 = MockVirtualPort(new_shape=self.new_shape) - virtual_port2 = MockVirtualPort(new_shape=self.new_shape) + virtual_port1 = MockVirtualPort(new_shape=self.new_shape, + axes=self.axes) + virtual_port2 = MockVirtualPort(new_shape=self.new_shape, + axes=self.axes) source1.out_port._connect_forward( [virtual_port1], AbstractPort, assert_same_shape=False @@ -223,10 +380,12 @@ def test_transpose_outport_to_inport(self) -> None: source.out_port.transpose(axes=self.axes).connect(sink.in_port) - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() + try: + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + finally: + sink.stop() expected = self.input_data.transpose(self.axes) self.assertTrue( @@ -265,16 +424,9 @@ def test_transpose_refport_read_from_varport(self) -> None: where the RefPort reads from the VarPort.""" source = RefPortReadProcess(data=np.zeros(self.shape)) - sink = VarPortProcess( - data=self.input_data.reshape(self.shape_transposed)) + sink = VarPortProcess(data=self.input_data.transpose(self.axes)) - virtual_port = MockVirtualPort(new_shape=self.shape_transposed, - old_shape=self.shape) - - source.ref_port._connect_forward( - [virtual_port], AbstractPort, assert_same_shape=False - ) - virtual_port.connect(sink.var_port) + source.ref_port.transpose(self.axes).connect(sink.var_port) try: sink.run(condition=RunSteps(num_steps=self.num_steps), @@ -310,10 +462,12 @@ def test_reshape_outport_to_inport(self) -> None: source.out_port.reshape(new_shape=self.shape_reshaped).connect( sink.in_port) - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() + try: + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + finally: + sink.stop() expected = self.input_data.reshape(self.shape_reshaped) self.assertTrue( @@ -354,13 +508,7 @@ def test_reshape_refport_read_from_varport(self) -> None: source = RefPortReadProcess(data=np.zeros(self.shape)) sink = VarPortProcess(data=self.input_data.reshape(self.shape_reshaped)) - virtual_port = MockVirtualPort(new_shape=self.shape_reshaped, - old_shape=self.shape) - - source.ref_port._connect_forward( - [virtual_port], AbstractPort, assert_same_shape=False - ) - virtual_port.connect(sink.var_port) + source.ref_port.reshape(self.shape_reshaped).connect(sink.var_port) try: sink.run(condition=RunSteps(num_steps=self.num_steps), @@ -397,10 +545,12 @@ def test_flatten_outport_to_inport(self) -> None: source.out_port.flatten().connect(sink.in_port) - sink.run(condition=RunSteps(num_steps=self.num_steps), - run_cfg=Loihi1SimCfg(select_tag='floating_pt')) - output = sink.data.get() - sink.stop() + try: + sink.run(condition=RunSteps(num_steps=self.num_steps), + run_cfg=Loihi1SimCfg(select_tag='floating_pt')) + output = sink.data.get() + finally: + sink.stop() expected = self.input_data.ravel() self.assertTrue( @@ -543,11 +693,13 @@ def __init__(self, proc): class HVPInPortProcess(AbstractProcess): def __init__(self, h_shape: ty.Tuple[int, ...], - s_shape: ty.Tuple[int, ...]) -> None: + s_shape: ty.Tuple[int, ...], + axes: ty.Tuple[int, ...]) -> None: super().__init__() self.s_data = Var(shape=s_shape, init=np.zeros(s_shape)) self.in_port = InPort(shape=h_shape) self.proc_params['s_shape'] = s_shape + self.proc_params['axes'] = axes # A minimal hierarchical ProcModel with a nested InPortProcess and an aliased @@ -557,7 +709,8 @@ class SubHVPInPortProcModel(AbstractSubProcessModel): def __init__(self, proc): self.in_proc = InPortProcess(shape=proc.proc_params['s_shape']) - virtual_port = MockVirtualPort(new_shape=proc.proc_params['s_shape']) + virtual_port = MockVirtualPort(new_shape=proc.proc_params['s_shape'], + axes=proc.proc_params['axes']) proc.in_port._connect_forward( [virtual_port], AbstractPort, assert_same_shape=False ) @@ -572,11 +725,13 @@ def __init__(self, proc): class HVPOutPortProcess(AbstractProcess): def __init__(self, h_shape: ty.Tuple[int, ...], - data: np.ndarray) -> None: + data: np.ndarray, + axes: ty.Tuple[int, ...]) -> None: super().__init__(h_shape=h_shape, data=data) self.out_port = OutPort(shape=h_shape) self.proc_params['data'] = data self.proc_params['h_shape'] = h_shape + self.proc_params['axes'] = axes # A minimal hierarchical ProcModel with a nested OutPortProcess @@ -585,7 +740,8 @@ class SubHVPOutPortProcModel(AbstractSubProcessModel): def __init__(self, proc): self.out_proc = OutPortProcess(data=proc.proc_params['data']) - virtual_port = MockVirtualPort(new_shape=proc.proc_params['h_shape']) + virtual_port = MockVirtualPort(new_shape=proc.proc_params['h_shape'], + axes=proc.proc_params['axes']) self.out_proc.out_port._connect_forward( [virtual_port], AbstractPort, assert_same_shape=False ) @@ -659,5 +815,103 @@ class PyVarPortProcessModelFloat(PyLoihiProcessModel): data: np.ndarray = LavaPyType(np.ndarray, np.int32) +# A minimal hierarchical Process with a RefPort, where the data that is +# given as an argument may have a different shape than the RefPort of the +# Process. +class HVPRefPortWriteProcess(AbstractProcess): + def __init__(self, + h_shape: ty.Tuple[int, ...], + data: np.ndarray, + axes: ty.Tuple[int, ...]) -> None: + super().__init__(h_shape=h_shape, data=data) + self.ref_port = RefPort(shape=h_shape) + self.proc_params['data'] = data + self.proc_params['h_shape'] = h_shape + self.proc_params['axes'] = axes + + +# A minimal hierarchical ProcModel with a nested RefPortWriteProcess +@implements(proc=HVPRefPortWriteProcess) +class SubHVPRefPortWriteProcModel(AbstractSubProcessModel): + def __init__(self, proc): + self.ref_write_proc = RefPortWriteProcess(data=proc.proc_params['data']) + + virtual_port = MockVirtualPort(new_shape=proc.proc_params['h_shape'], + axes=proc.proc_params['axes']) + self.ref_write_proc.ref_port._connect_forward( + [virtual_port], AbstractPort, assert_same_shape=False + ) + virtual_port.connect(proc.ref_port) + + +# A minimal hierarchical Process with a RefPort, where the data that is +# given as an argument may have a different shape than the RefPort of the +# Process. +class HVPRefPortReadProcess(AbstractProcess): + def __init__(self, + h_shape: ty.Tuple[int, ...], + s_shape: ty.Tuple[int, ...], + axes: ty.Tuple[int, ...]) -> None: + super().__init__(h_shape=h_shape, s_shape=s_shape) + self.ref_port = RefPort(shape=h_shape) + self.s_data = Var(s_shape) + self.proc_params['s_shape'] = s_shape + self.proc_params['h_shape'] = h_shape + self.proc_params['axes'] = axes + + +# A minimal hierarchical ProcModel with a nested RefPortReadProcess +@implements(proc=HVPRefPortReadProcess) +class SubHVPRefPortReadProcModel(AbstractSubProcessModel): + def __init__(self, proc): + self.ref_read_proc = \ + RefPortReadProcess(data=np.zeros(proc.proc_params['s_shape'])) + + virtual_port = MockVirtualPort(new_shape=proc.proc_params['h_shape'], + axes=proc.proc_params['axes']) + self.ref_read_proc.ref_port._connect_forward( + [virtual_port], AbstractPort, assert_same_shape=False + ) + virtual_port.connect(proc.ref_port) + + proc.s_data.alias(self.ref_read_proc.data) + + +# A minimal hierarchical Process with a VarPort, where the data that is +# given as an argument may have a different shape than the VarPort of the +# Process. +class HVPVarPortProcess(AbstractProcess): + def __init__(self, + h_shape: ty.Tuple[int, ...], + s_data: np.ndarray, + axes: ty.Tuple[int, ...]) -> None: + super().__init__(h_shape=h_shape, s_data=s_data) + self.h_data = Var(h_shape) + self.s_data = Var(s_data.shape) + self.var_port = VarPort(self.h_data) + + self.proc_params['s_data'] = s_data + self.proc_params['h_shape'] = h_shape + self.proc_params['axes'] = axes + + +# A minimal hierarchical ProcModel with a nested RefPortReadProcess +@implements(proc=HVPVarPortProcess) +class SubHVPVarPortProcModel(AbstractSubProcessModel): + def __init__(self, proc): + s_data = proc.proc_params['s_data'] + self.var_proc = \ + VarPortProcess(data=s_data) + + virtual_port = MockVirtualPort(new_shape=s_data.shape, + axes=proc.proc_params['axes']) + proc.var_port._connect_forward( + [virtual_port], AbstractPort, assert_same_shape=False + ) + virtual_port.connect(self.var_proc.var_port) + + proc.s_data.alias(self.var_proc.data) + + if __name__ == '__main__': unittest.main() From 83fd8d9736346a615b892291ef80b881936f07c4 Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Tue, 1 Mar 2022 21:16:12 +0100 Subject: [PATCH 20/21] Fixed a docstring Signed-off-by: Mathis Richter --- .../magma/core/process/ports/test_virtual_ports_in_process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py index ad838e062..deb9cef91 100644 --- a/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py +++ b/tests/lava/magma/core/process/ports/test_virtual_ports_in_process.py @@ -37,7 +37,7 @@ class MockVirtualPort(AbstractVirtualPort, AbstractPort): - """A mock-up of a virtual port that reshapes the input.""" + """A mock-up of a virtual port that permutes the axes of the input.""" def __init__(self, new_shape: ty.Tuple[int, ...], axes: ty.Tuple[int, ...]): From eaf55ca17da722f400ee9a194c2c1a49b60ebb15 Mon Sep 17 00:00:00 2001 From: Mathis Richter Date: Wed, 2 Mar 2022 15:21:56 +0100 Subject: [PATCH 21/21] Added docstrings to methods get_transform_func_fwd/bwd. Signed-off-by: Mathis Richter --- src/lava/magma/core/process/ports/ports.py | 48 ++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/lava/magma/core/process/ports/ports.py b/src/lava/magma/core/process/ports/ports.py index 83a0d2a0d..a3e0c6792 100644 --- a/src/lava/magma/core/process/ports/ports.py +++ b/src/lava/magma/core/process/ports/ports.py @@ -716,10 +716,24 @@ def connect(self, ports: ty.Union["AbstractPort", ty.List["AbstractPort"]]): @abstractmethod def get_transform_func_fwd(self) -> ft.partial: + """Returns a function pointer that implements the forward (fwd) + transformation of the virtual port. + + Returns + ------- + function_pointer : functools.partial + a function pointer that can be applied to incoming data""" pass @abstractmethod def get_transform_func_bwd(self) -> ft.partial: + """Returns a function pointer that implements the backward (bwd) + transformation of the virtual port. + + Returns + ------- + function_pointer : functools.partial + a function pointer that can be applied to incoming data""" pass @@ -736,9 +750,25 @@ def __init__(self, self.old_shape = old_shape def get_transform_func_fwd(self) -> ft.partial: + """Returns a function pointer that implements the forward (fwd) + transformation of the ReshapePort, which reshapes incoming data to + a new shape (the shape of the destination Process). + + Returns + ------- + function_pointer : functools.partial + a function pointer that can be applied to incoming data""" return ft.partial(np.reshape, newshape=self.shape) def get_transform_func_bwd(self) -> ft.partial: + """Returns a function pointer that implements the backward (bwd) + transformation of the ReshapePort, which reshapes incoming data to + a new shape (the shape of the source Process). + + Returns + ------- + function_pointer : functools.partial + a function pointer that can be applied to incoming data""" return ft.partial(np.reshape, newshape=self.old_shape) @@ -811,9 +841,27 @@ def __init__(self, AbstractPort.__init__(self, new_shape) def get_transform_func_fwd(self) -> ft.partial: + """Returns a function pointer that implements the forward (fwd) + transformation of the TransposePort, which transposes (permutes) + incoming data according to a specific order of axes (to match the + destination Process). + + Returns + ------- + function_pointer : functools.partial + a function pointer that can be applied to incoming data""" return ft.partial(np.transpose, axes=self.axes) def get_transform_func_bwd(self) -> ft.partial: + """Returns a function pointer that implements the backward (bwd) + transformation of the TransposePort, which transposes (permutes) + incoming data according to a specific order of axes (to match the + source Process). + + Returns + ------- + function_pointer : functools.partial + a function pointer that can be applied to incoming data""" return ft.partial(np.transpose, axes=np.argsort(self.axes))