From 57feec29bd99f31dfa7592ae5cc344bbc1afca88 Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Fri, 3 Mar 2023 20:31:45 +0100 Subject: [PATCH 01/18] remove dependencies --- limbus/core/component.py | 14 ++++++++++---- limbus/core/param.py | 18 ++++++++++++++++-- setup.py | 6 +++--- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/limbus/core/component.py b/limbus/core/component.py index 7520a3b..0bc2253 100644 --- a/limbus/core/component.py +++ b/limbus/core/component.py @@ -1,13 +1,17 @@ """Component definition.""" from __future__ import annotations from abc import abstractmethod -from typing import List, Optional, TYPE_CHECKING, Callable +from typing import List, Optional, TYPE_CHECKING, Callable, Type import logging import asyncio import traceback import functools -import torch.nn as nn +try: + import torch.nn as nn + base_cls: Type = nn.Module +except ImportError: + base_cls = object from limbus.core.params import Params, InputParams, OutputParams from limbus.core.states import ComponentState, ComponentStoppedError @@ -15,9 +19,11 @@ if TYPE_CHECKING: from limbus.core.pipeline import Pipeline - log = logging.getLogger(__name__) +class Module(base_cls): # noqa: D101 + pass + # this is a decorator that will determine how many iterations must be run def iterations_manager(func: Callable) -> Callable: @@ -86,7 +92,7 @@ def verbose(self, value: bool) -> None: self._verbose = value -class Component(nn.Module): +class Component(Module): """Base class to define a Limbus Component. Args: diff --git a/limbus/core/param.py b/limbus/core/param.py index 344f552..2ccf9c6 100644 --- a/limbus/core/param.py +++ b/limbus/core/param.py @@ -10,7 +10,21 @@ import contextlib import typeguard -import torch + +SUBSCRIPTABLE_TYPES: List[type] = [] +try: + import torch + SUBSCRIPTABLE_TYPES.append(torch.Tensor) +except ImportError: + pass + +try: + import numpy as np + SUBSCRIPTABLE_TYPES.append(np.ndarray) +except ImportError: + pass + + # Note that Component class cannot be imported to avoid circular dependencies. if TYPE_CHECKING: @@ -120,7 +134,7 @@ def _check_subscriptable(datatype: type) -> bool: # mypy complaints in the case origin is NoneType if is_abstract_seq or (not is_abstract and isinstance(origin(), typing.Iterable)): # type: ignore if (len(datatype_args) == 1 or (len(datatype_args) == 2 and Ellipsis in datatype_args)): - if datatype_args[0] is torch.Tensor: + if datatype_args[0] in SUBSCRIPTABLE_TYPES: return True return False diff --git a/setup.py b/setup.py index eed2a9b..0c14790 100644 --- a/setup.py +++ b/setup.py @@ -8,10 +8,7 @@ author='Luis Ferraz', url='https://github.com/kornia/limbus', install_requires=[ - 'torch', - 'numpy', 'typeguard', - 'kornia', ], extras_require={ 'dev': [ @@ -30,6 +27,9 @@ 'limbus-components' ], 'widgets': [ + 'kornia', + 'torch', + 'numpy', 'visdom', 'opencv-python', ] From be5c9fa2b74a5be7f8ac1efc4935c94d66624d81 Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Fri, 3 Mar 2023 21:43:00 +0100 Subject: [PATCH 02/18] make optional the imports for widgets --- limbus/widgets/types.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/limbus/widgets/types.py b/limbus/widgets/types.py index fa86c9e..44b6d6c 100644 --- a/limbus/widgets/types.py +++ b/limbus/widgets/types.py @@ -6,16 +6,17 @@ import functools try: - # NOTE: we import the cv2 & visdom modules here to avoid having it as a dependency + # NOTE: we import these modules here to avoid having it as a dependency # for the whole project. import cv2 import visdom + import torch + import kornia + import numpy as np except ImportError: pass -import torch -import kornia -import numpy as np + from limbus.core import Component from limbus import widgets @@ -121,7 +122,7 @@ def check_status(self) -> bool: raise NotImplementedError @abstractmethod - def show_image(self, component: Component, title: str, image: torch.Tensor): + def show_image(self, component: Component, title: str, image: "torch.Tensor"): """Show an image. Args: @@ -134,7 +135,7 @@ def show_image(self, component: Component, title: str, image: torch.Tensor): @abstractmethod def show_images(self, component: Component, title: str, - images: Union[torch.Tensor, List[torch.Tensor]], + images: Union["torch.Tensor", List["torch.Tensor"]], nrow: Optional[int] = None ) -> None: """Show a batch of images. @@ -203,7 +204,7 @@ def check_status(self) -> bool: @is_enabled @set_title - def show_image(self, component: Component, title: str, image: torch.Tensor) -> None: + def show_image(self, component: Component, title: str, image: "torch.Tensor") -> None: """Show an image. Args: @@ -219,7 +220,7 @@ def show_image(self, component: Component, title: str, image: torch.Tensor) -> N @is_enabled @set_title def show_images(self, component: Component, title: str, - images: Union[torch.Tensor, List[torch.Tensor]], + images: Union["torch.Tensor", List["torch.Tensor"]], nrow: Optional[int] = None ) -> None: """Show a batch of images. @@ -269,7 +270,7 @@ def check_status(self) -> bool: @is_enabled @set_title - def show_image(self, component: Component, title: str, image: torch.Tensor): + def show_image(self, component: Component, title: str, image: "torch.Tensor"): """Show an image. Args: @@ -283,7 +284,7 @@ def show_image(self, component: Component, title: str, image: torch.Tensor): @is_enabled @set_title def show_images(self, component: Component, title: str, - images: Union[torch.Tensor, List[torch.Tensor]], + images: Union["torch.Tensor", List["torch.Tensor"]], nrow: Optional[int] = None ) -> None: """Show a batch of images. @@ -326,7 +327,7 @@ def __init__(self) -> None: @is_enabled @set_title - def show_image(self, component: Component, title: str, image: torch.Tensor): + def show_image(self, component: Component, title: str, image: "torch.Tensor"): """Show an image. Args: @@ -357,7 +358,7 @@ def show_image(self, component: Component, title: str, image: torch.Tensor): @is_enabled @set_title def show_images(self, component: Component, title: str, - images: Union[torch.Tensor, List[torch.Tensor]], + images: Union["torch.Tensor", List["torch.Tensor"]], nrow: Optional[int] = None ) -> None: """Show a batch of images. From 98363cee2cdd3624a665f83c773c88c53080d925 Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Fri, 3 Mar 2023 21:43:26 +0100 Subject: [PATCH 03/18] improve example with default components --- examples/default_cmps.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/examples/default_cmps.py b/examples/default_cmps.py index 5e4bdfd..0c58f89 100644 --- a/examples/default_cmps.py +++ b/examples/default_cmps.py @@ -1,5 +1,7 @@ """Basic example with predefined cmps.""" import asyncio +from sys import version_info +import copy import torch @@ -25,15 +27,24 @@ t2.outputs.out >> stack.inputs.tensors.select(1) stack.outputs.tensor >> show.inputs.inp -# create the pipeline and add its nodes -pipeline = Pipeline() -pipeline.add_nodes([c1, t1, t2, stack, show]) - -# run your pipeline (only one iteration, note that this pipeline can run forever) -print("Run with pipeline:") -pipeline.run(1) - -# run 1 iteration using the asyncio loop -print("Run with loop:") -loop = asyncio.get_event_loop() -loop.run_until_complete(asyncio.gather(c1(), t1(), t2(), stack(), show())) +USING_PIPELINE = True +if USING_PIPELINE: + # run your pipeline (only one iteration, note that this pipeline can run forever) + print("Run with pipeline:") + # create the pipeline and add its nodes + pipeline = Pipeline() + pipeline.add_nodes([c1, t1, t2, stack, show]) + pipeline.run(1) +else: + # run 1 iteration using the asyncio loop + print("Run with loop:") + async def f(): # noqa: D103 + await asyncio.gather(c1(), t1(), t2(), stack(), show()) + + if version_info.minor < 10: + # for python <3.10 the loop must be run in this way to avoid creating a new loop. + loop = asyncio.get_event_loop() + loop.run_until_complete(f()) + elif version_info.minor >= 10: + # for python >=3.10 the loop should be run in this way. + asyncio.run(f()) From fb4ec4162f779b537f6df2dd17c34d4440548ca1 Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Fri, 3 Mar 2023 21:44:26 +0100 Subject: [PATCH 04/18] when not nn.module inheritance call directly teh forward method in the component --- limbus/core/component.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/limbus/core/component.py b/limbus/core/component.py index 0bc2253..208d094 100644 --- a/limbus/core/component.py +++ b/limbus/core/component.py @@ -21,6 +21,7 @@ log = logging.getLogger(__name__) + class Module(base_cls): # noqa: D101 pass @@ -317,7 +318,11 @@ async def _run_with_hooks(self, *args, **kwargs) -> bool: if len(self._inputs) == 0: # RUNNING state is set once the input params are received, if there are not inputs the state is set here self.set_state(ComponentState.RUNNING) - self.set_state(await super().__call__(*args, **kwargs)) # internally it calls the forward() method + if hasattr(super(), "__call__"): + # If the component inherits from nn.Module, the forward method is called by the __call__ method + self.set_state(await super().__call__(*args, **kwargs)) + else: + self.set_state(await self.forward(*args, **kwargs)) except ComponentStoppedError as e: self.set_state(e.state) except Exception as e: From 8be5af213a6f5e0e2eedfa1c555a3e20b52ece6b Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Fri, 3 Mar 2023 21:44:48 +0100 Subject: [PATCH 05/18] reorder imports in param --- limbus/core/param.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/limbus/core/param.py b/limbus/core/param.py index 2ccf9c6..66dff82 100644 --- a/limbus/core/param.py +++ b/limbus/core/param.py @@ -11,6 +11,12 @@ import typeguard +from limbus.core.states import ComponentState, ComponentStoppedError +from limbus.core import async_utils +# Note that Component class cannot be imported to avoid circular dependencies. +if TYPE_CHECKING: + from limbus.core.component import Component + SUBSCRIPTABLE_TYPES: List[type] = [] try: import torch @@ -25,14 +31,6 @@ pass - -# Note that Component class cannot be imported to avoid circular dependencies. -if TYPE_CHECKING: - from limbus.core.component import Component -from limbus.core.states import ComponentState, ComponentStoppedError -from limbus.core import async_utils - - class NoValue: """Denote that a param does not have a value.""" pass From d316c10a00622be7598259b17aeee7b192d26d9c Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Fri, 3 Mar 2023 21:47:46 +0100 Subject: [PATCH 06/18] flake --- examples/default_cmps.py | 1 + limbus/widgets/types.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/default_cmps.py b/examples/default_cmps.py index 0c58f89..a3d511b 100644 --- a/examples/default_cmps.py +++ b/examples/default_cmps.py @@ -38,6 +38,7 @@ else: # run 1 iteration using the asyncio loop print("Run with loop:") + async def f(): # noqa: D103 await asyncio.gather(c1(), t1(), t2(), stack(), show()) diff --git a/limbus/widgets/types.py b/limbus/widgets/types.py index 44b6d6c..153f0eb 100644 --- a/limbus/widgets/types.py +++ b/limbus/widgets/types.py @@ -17,7 +17,6 @@ pass - from limbus.core import Component from limbus import widgets From ebf8070b1c55df4c08184f9edfb9961772f8f42e Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Mon, 6 Mar 2023 23:59:53 +0100 Subject: [PATCH 07/18] improve base class check --- limbus/core/component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/limbus/core/component.py b/limbus/core/component.py index 208d094..4a0525d 100644 --- a/limbus/core/component.py +++ b/limbus/core/component.py @@ -318,7 +318,7 @@ async def _run_with_hooks(self, *args, **kwargs) -> bool: if len(self._inputs) == 0: # RUNNING state is set once the input params are received, if there are not inputs the state is set here self.set_state(ComponentState.RUNNING) - if hasattr(super(), "__call__"): + if base_cls != object: # at this moment the base class can be only nn.Module or object # If the component inherits from nn.Module, the forward method is called by the __call__ method self.set_state(await super().__call__(*args, **kwargs)) else: From 6c49d47c6908d0d78e4bf216a5ee42efc526c0dd Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Wed, 8 Mar 2023 19:59:29 +0100 Subject: [PATCH 08/18] TorchComponent inherits from nn-Module and Component --- limbus/core/__init__.py | 3 ++- limbus/core/component.py | 29 +++++++++++--------- tests/core/test_component.py | 51 +++++++++++++++++++++--------------- 3 files changed, 49 insertions(+), 34 deletions(-) diff --git a/limbus/core/__init__.py b/limbus/core/__init__.py index bf12b30..fcbcf0d 100644 --- a/limbus/core/__init__.py +++ b/limbus/core/__init__.py @@ -1,4 +1,4 @@ -from limbus.core.component import Component, iterations_manager +from limbus.core.component import Component, TorchComponent, iterations_manager from limbus.core.states import ComponentState, PipelineState, VerboseMode from limbus.core.param import NoValue, Param, Reference from limbus.core.params import Params, InputParams, OutputParams @@ -12,6 +12,7 @@ "PipelineState", "VerboseMode", "Component", + "TorchComponent", "iterations_manager", "ComponentState", "Params", diff --git a/limbus/core/component.py b/limbus/core/component.py index 4a0525d..ee4fd07 100644 --- a/limbus/core/component.py +++ b/limbus/core/component.py @@ -9,9 +9,8 @@ try: import torch.nn as nn - base_cls: Type = nn.Module except ImportError: - base_cls = object + pass from limbus.core.params import Params, InputParams, OutputParams from limbus.core.states import ComponentState, ComponentStoppedError @@ -22,10 +21,6 @@ log = logging.getLogger(__name__) -class Module(base_cls): # noqa: D101 - pass - - # this is a decorator that will determine how many iterations must be run def iterations_manager(func: Callable) -> Callable: """Update the last iteration to be run by the component.""" @@ -93,7 +88,7 @@ def verbose(self, value: bool) -> None: self._verbose = value -class Component(Module): +class Component: """Base class to define a Limbus Component. Args: @@ -318,11 +313,7 @@ async def _run_with_hooks(self, *args, **kwargs) -> bool: if len(self._inputs) == 0: # RUNNING state is set once the input params are received, if there are not inputs the state is set here self.set_state(ComponentState.RUNNING) - if base_cls != object: # at this moment the base class can be only nn.Module or object - # If the component inherits from nn.Module, the forward method is called by the __call__ method - self.set_state(await super().__call__(*args, **kwargs)) - else: - self.set_state(await self.forward(*args, **kwargs)) + self.set_state(await self._run_forward(*args, **kwargs)) except ComponentStoppedError as e: self.set_state(e.state) except Exception as e: @@ -338,6 +329,9 @@ async def _run_with_hooks(self, *args, **kwargs) -> bool: # if there is not a pipeline, the component is executed only once return True + async def _run_forward(self, *args, **kwargs) -> ComponentState: + return await self.forward(*args, **kwargs) + @abstractmethod async def forward(self, *args, **kwargs) -> ComponentState: """Run the component, this method shouldn't be called, instead call __call__.""" @@ -375,3 +369,14 @@ def init_pipeline(self) -> None: """ pass + + +try: + class TorchComponent(Component, nn.Module): + """Base class for all the components that inherit from nn.Module.""" + async def _run_forward(self, *args, **kwargs) -> ComponentState: + # If the component inherits from nn.Module, the forward method is called by the __call__ method + return await nn.Module.__call__(self, *args, **kwargs) +except NameError: + # if nn.Module is not defined, the class is not defined + pass diff --git a/tests/core/test_component.py b/tests/core/test_component.py index 63e95fc..12346ef 100644 --- a/tests/core/test_component.py +++ b/tests/core/test_component.py @@ -1,7 +1,7 @@ import pytest import logging -from limbus.core import Component, ComponentState, Pipeline +from limbus.core import Component, ComponentState, Pipeline, TorchComponent from limbus.core.component import _ComponentState @@ -35,18 +35,20 @@ def test_call(self, caplog, verbose): class TestComponent: - def test_smoke(self): - cmp = Component("yuhu") + @pytest.mark.parametrize("cls", [Component, TorchComponent]) + def test_smoke(self, cls): + cmp = cls("yuhu") assert cmp.name == "yuhu" assert cmp.inputs is not None assert cmp.outputs is not None assert cmp.properties is not None - def test_set_properties(self): - class A(Component): + @pytest.mark.parametrize("cls", [Component, TorchComponent]) + def test_set_properties(self, cls): + class A(cls): @staticmethod def register_properties(properties): - Component.register_properties(properties) + cls.register_properties(properties) properties.declare("a", float, 1.) properties.declare("b", float, 2.) @@ -69,8 +71,9 @@ def register_properties(properties): assert p["a"] == float assert p["b"] == float - def test_register_inputs(self): - class A(Component): + @pytest.mark.parametrize("cls", [Component, TorchComponent]) + def test_register_inputs(self, cls): + class A(cls): @staticmethod def register_inputs(inputs): inputs.declare("a", float, 1.) @@ -82,8 +85,9 @@ def register_inputs(inputs): assert cmp.inputs.a.value == 1. assert cmp.inputs.b.value == 2. - def test_register_outputs(self): - class A(Component): + @pytest.mark.parametrize("cls", [Component, TorchComponent]) + def test_register_outputs(self, cls): + class A(cls): @staticmethod def register_outputs(outputs): outputs.declare("a", float, 1.) @@ -95,8 +99,9 @@ def register_outputs(outputs): assert cmp.outputs.a.value == 1. assert cmp.outputs.b.value == 2. - def test_init_from_component(self): - class A(Component): + @pytest.mark.parametrize("cls", [Component, TorchComponent]) + def test_init_from_component(self, cls): + class A(cls): pass cmp = A("yuhu") @@ -110,8 +115,9 @@ class A(Component): @pytest.mark.usefixtures("event_loop_instance") class TestComponentWithPipeline: - def test_init_from_component_with_pipeline(self): - class A(Component): + @pytest.mark.parametrize("cls", [Component, TorchComponent]) + def test_init_from_component_with_pipeline(self, cls): + class A(cls): @staticmethod def register_outputs(outputs): outputs.declare("out", int) @@ -120,7 +126,7 @@ async def forward(): self._outputs.out.send(1) return ComponentState.OK - class B(Component): + class B(cls): @staticmethod def register_inputs(inputs): inputs.declare("inp", int) @@ -146,8 +152,9 @@ async def forward(): assert b in pipeline._nodes @pytest.mark.parametrize("iters", [0, 1, 2]) - def test_get_stopping_iteration(self, iters): - class A(Component): + @pytest.mark.parametrize("cls", [Component, TorchComponent]) + def test_get_stopping_iteration(self, iters, cls): + class A(cls): async def forward(self): return ComponentState.STOPPED @@ -159,8 +166,9 @@ async def forward(self): assert pipeline.counter == 1 assert cmp.stopping_iteration == iters - def test_stop_after_exception(self): - class A(Component): + @pytest.mark.parametrize("cls", [Component, TorchComponent]) + def test_stop_after_exception(self, cls): + class A(cls): async def forward(self): raise Exception("test") @@ -172,8 +180,9 @@ async def forward(self): assert pipeline.counter == 1 assert cmp.state == ComponentState.ERROR - def test_stop_after_stop(self): - class A(Component): + @pytest.mark.parametrize("cls", [Component, TorchComponent]) + def test_stop_after_stop(self, cls): + class A(cls): async def forward(self): return ComponentState.STOPPED From 9fa1c3856276b62beb0f509fb0e31be8f274af20 Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Thu, 9 Mar 2023 01:23:47 +0100 Subject: [PATCH 09/18] new proposal for the Component inheritance --- examples/defining_cmps.py | 4 ++++ limbus/core/__init__.py | 4 ++-- limbus/core/component.py | 49 +++++++++++++++++++++++++++++++++------ 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/examples/defining_cmps.py b/examples/defining_cmps.py index 26b4a49..2c4d05e 100644 --- a/examples/defining_cmps.py +++ b/examples/defining_cmps.py @@ -2,6 +2,10 @@ from typing import List, Any import asyncio +# example of how to change the base class for the component +from limbus import core +core.Component = core.set_component_base_class() + from limbus.core import Component, Params, InputParams, OutputParams, ComponentState, VerboseMode from limbus.core.pipeline import Pipeline diff --git a/limbus/core/__init__.py b/limbus/core/__init__.py index fcbcf0d..87ad636 100644 --- a/limbus/core/__init__.py +++ b/limbus/core/__init__.py @@ -1,4 +1,4 @@ -from limbus.core.component import Component, TorchComponent, iterations_manager +from limbus.core.component import Component, iterations_manager, set_component_base_class from limbus.core.states import ComponentState, PipelineState, VerboseMode from limbus.core.param import NoValue, Param, Reference from limbus.core.params import Params, InputParams, OutputParams @@ -12,7 +12,7 @@ "PipelineState", "VerboseMode", "Component", - "TorchComponent", + "set_component_base_class", "iterations_manager", "ComponentState", "Params", diff --git a/limbus/core/component.py b/limbus/core/component.py index ee4fd07..4f14d96 100644 --- a/limbus/core/component.py +++ b/limbus/core/component.py @@ -1,7 +1,7 @@ """Component definition.""" from __future__ import annotations from abc import abstractmethod -from typing import List, Optional, TYPE_CHECKING, Callable, Type +from typing import List, Optional, TYPE_CHECKING, Callable, Type, Union import logging import asyncio import traceback @@ -41,9 +41,9 @@ class _ComponentState(): verbose (bool, optional): verbose state. Default: False. """ - def __init__(self, component: Component, state: ComponentState, verbose: bool = False): + def __init__(self, component: BaseComponent, state: ComponentState, verbose: bool = False): self._state: ComponentState = state - self._component: Component = component + self._component: BaseComponent = component self._verbose: bool = verbose def __call__(self, state: Optional[ComponentState] = None, msg: Optional[str] = None) -> ComponentState: @@ -88,7 +88,7 @@ def verbose(self, value: bool) -> None: self._verbose = value -class Component: +class BaseComponent: """Base class to define a Limbus Component. Args: @@ -112,7 +112,7 @@ def __init__(self, name: str): # Last execution to be run in the __call__ loop. self._stopping_iteration: int = 0 # 0 means run forever - def init_from_component(self, ref_component: Component) -> None: + def init_from_component(self, ref_component: BaseComponent) -> None: """Init basic execution params from another component. Args: @@ -372,11 +372,46 @@ def init_pipeline(self) -> None: try: - class TorchComponent(Component, nn.Module): + class TorchComponent(BaseComponent, nn.Module): """Base class for all the components that inherit from nn.Module.""" + def __init__(self, name: str): + BaseComponent.__init__(self, name) + nn.Module.__init__(self) + async def _run_forward(self, *args, **kwargs) -> ComponentState: # If the component inherits from nn.Module, the forward method is called by the __call__ method return await nn.Module.__call__(self, *args, **kwargs) except NameError: - # if nn.Module is not defined, the class is not defined pass + +def set_component_base_class(base_class: Type = object): # -> Type[Union[TorchComponent, BaseComponent]]: + """Get a component class inheriting from a given base class. + + At this moment only nn.Module and object are supported. + When limbus module is imported calling this method the base class of the component can be changed. + E.g.: `limbus.Componet = limbus.get_component_class()`. + + Args: + base_class: Base class of the component. + + Returns: + Component class. + + """ + try: + if base_class == nn.Module: + return TorchComponent + except: + pass + if base_class == object: + return BaseComponent + raise ValueError(f"Base class {base_class} not supported. Must be nn.Module or object.") + +# By default the component inherits from nn.Module if it is available. +# To change this behaviour when limbus is imported add the next lines: +# from limbus import core +# core.Component = core.set_component_base_class() +try: + Component = set_component_base_class(nn.Module) +except: + Component = set_component_base_class() From bd0e943d1441d6d5a61b7bb0bc284aecb7439f1e Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Thu, 9 Mar 2023 01:30:18 +0100 Subject: [PATCH 10/18] rollback component tests --- tests/core/test_component.py | 51 +++++++++++++++--------------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/tests/core/test_component.py b/tests/core/test_component.py index 12346ef..63e95fc 100644 --- a/tests/core/test_component.py +++ b/tests/core/test_component.py @@ -1,7 +1,7 @@ import pytest import logging -from limbus.core import Component, ComponentState, Pipeline, TorchComponent +from limbus.core import Component, ComponentState, Pipeline from limbus.core.component import _ComponentState @@ -35,20 +35,18 @@ def test_call(self, caplog, verbose): class TestComponent: - @pytest.mark.parametrize("cls", [Component, TorchComponent]) - def test_smoke(self, cls): - cmp = cls("yuhu") + def test_smoke(self): + cmp = Component("yuhu") assert cmp.name == "yuhu" assert cmp.inputs is not None assert cmp.outputs is not None assert cmp.properties is not None - @pytest.mark.parametrize("cls", [Component, TorchComponent]) - def test_set_properties(self, cls): - class A(cls): + def test_set_properties(self): + class A(Component): @staticmethod def register_properties(properties): - cls.register_properties(properties) + Component.register_properties(properties) properties.declare("a", float, 1.) properties.declare("b", float, 2.) @@ -71,9 +69,8 @@ def register_properties(properties): assert p["a"] == float assert p["b"] == float - @pytest.mark.parametrize("cls", [Component, TorchComponent]) - def test_register_inputs(self, cls): - class A(cls): + def test_register_inputs(self): + class A(Component): @staticmethod def register_inputs(inputs): inputs.declare("a", float, 1.) @@ -85,9 +82,8 @@ def register_inputs(inputs): assert cmp.inputs.a.value == 1. assert cmp.inputs.b.value == 2. - @pytest.mark.parametrize("cls", [Component, TorchComponent]) - def test_register_outputs(self, cls): - class A(cls): + def test_register_outputs(self): + class A(Component): @staticmethod def register_outputs(outputs): outputs.declare("a", float, 1.) @@ -99,9 +95,8 @@ def register_outputs(outputs): assert cmp.outputs.a.value == 1. assert cmp.outputs.b.value == 2. - @pytest.mark.parametrize("cls", [Component, TorchComponent]) - def test_init_from_component(self, cls): - class A(cls): + def test_init_from_component(self): + class A(Component): pass cmp = A("yuhu") @@ -115,9 +110,8 @@ class A(cls): @pytest.mark.usefixtures("event_loop_instance") class TestComponentWithPipeline: - @pytest.mark.parametrize("cls", [Component, TorchComponent]) - def test_init_from_component_with_pipeline(self, cls): - class A(cls): + def test_init_from_component_with_pipeline(self): + class A(Component): @staticmethod def register_outputs(outputs): outputs.declare("out", int) @@ -126,7 +120,7 @@ async def forward(): self._outputs.out.send(1) return ComponentState.OK - class B(cls): + class B(Component): @staticmethod def register_inputs(inputs): inputs.declare("inp", int) @@ -152,9 +146,8 @@ async def forward(): assert b in pipeline._nodes @pytest.mark.parametrize("iters", [0, 1, 2]) - @pytest.mark.parametrize("cls", [Component, TorchComponent]) - def test_get_stopping_iteration(self, iters, cls): - class A(cls): + def test_get_stopping_iteration(self, iters): + class A(Component): async def forward(self): return ComponentState.STOPPED @@ -166,9 +159,8 @@ async def forward(self): assert pipeline.counter == 1 assert cmp.stopping_iteration == iters - @pytest.mark.parametrize("cls", [Component, TorchComponent]) - def test_stop_after_exception(self, cls): - class A(cls): + def test_stop_after_exception(self): + class A(Component): async def forward(self): raise Exception("test") @@ -180,9 +172,8 @@ async def forward(self): assert pipeline.counter == 1 assert cmp.state == ComponentState.ERROR - @pytest.mark.parametrize("cls", [Component, TorchComponent]) - def test_stop_after_stop(self, cls): - class A(cls): + def test_stop_after_stop(self): + class A(Component): async def forward(self): return ComponentState.STOPPED From 7436526b9664aeb121fb925e6c46e2106844402c Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Fri, 10 Mar 2023 02:23:27 +0100 Subject: [PATCH 11/18] use config file --- examples/defining_cmps.py | 10 +++--- limbus/__init__.py | 2 -- limbus/config.py | 11 ++++++ limbus/core/__init__.py | 3 +- limbus/core/component.py | 72 ++++++++++++--------------------------- 5 files changed, 38 insertions(+), 60 deletions(-) create mode 100644 limbus/config.py diff --git a/examples/defining_cmps.py b/examples/defining_cmps.py index 2c4d05e..8b66561 100644 --- a/examples/defining_cmps.py +++ b/examples/defining_cmps.py @@ -2,12 +2,12 @@ from typing import List, Any import asyncio -# example of how to change the base class for the component -from limbus import core -core.Component = core.set_component_base_class() +# If you want to change the limbus config you need to do it before importing any limbus module!!! +import limbus.config as config +config.COMPONENT_TYPE = "torch" -from limbus.core import Component, Params, InputParams, OutputParams, ComponentState, VerboseMode -from limbus.core.pipeline import Pipeline +from limbus.core import Component, Params, InputParams, OutputParams, ComponentState, VerboseMode # noqa: E402 +from limbus.core.pipeline import Pipeline # noqa: E402 # define the components diff --git a/limbus/__init__.py b/limbus/__init__.py index 9845c8b..e69de29 100644 --- a/limbus/__init__.py +++ b/limbus/__init__.py @@ -1,2 +0,0 @@ -from limbus import widgets -from limbus.core import Pipeline, Component diff --git a/limbus/config.py b/limbus/config.py new file mode 100644 index 0000000..1e3e29d --- /dev/null +++ b/limbus/config.py @@ -0,0 +1,11 @@ +"""Configuration file for Limbus. + +Usage: + Before importing any other Limbus module, set the COMPONENT_TYPE variable as: + > import limbus.config as config + > config.COMPONENT_TYPE = "torch" + + +""" + +COMPONENT_TYPE = "generic" # generic or torch diff --git a/limbus/core/__init__.py b/limbus/core/__init__.py index 87ad636..bf12b30 100644 --- a/limbus/core/__init__.py +++ b/limbus/core/__init__.py @@ -1,4 +1,4 @@ -from limbus.core.component import Component, iterations_manager, set_component_base_class +from limbus.core.component import Component, iterations_manager from limbus.core.states import ComponentState, PipelineState, VerboseMode from limbus.core.param import NoValue, Param, Reference from limbus.core.params import Params, InputParams, OutputParams @@ -12,7 +12,6 @@ "PipelineState", "VerboseMode", "Component", - "set_component_base_class", "iterations_manager", "ComponentState", "Params", diff --git a/limbus/core/component.py b/limbus/core/component.py index 4f14d96..6a64b1d 100644 --- a/limbus/core/component.py +++ b/limbus/core/component.py @@ -12,6 +12,7 @@ except ImportError: pass +from limbus import config from limbus.core.params import Params, InputParams, OutputParams from limbus.core.states import ComponentState, ComponentStoppedError # Note that Pipeline class cannot be imported to avoid circular dependencies. @@ -21,6 +22,18 @@ log = logging.getLogger(__name__) +if config.COMPONENT_TYPE == "generic": + base_class: Type = object +elif config.COMPONENT_TYPE == "torch": + try: + base_class = nn.Module + except NameError: + log.error("Torch not installed. Using generic base class.") +else: + base_class = object + log.error("Invalid component type. Using generic base class.") + + # this is a decorator that will determine how many iterations must be run def iterations_manager(func: Callable) -> Callable: """Update the last iteration to be run by the component.""" @@ -41,9 +54,9 @@ class _ComponentState(): verbose (bool, optional): verbose state. Default: False. """ - def __init__(self, component: BaseComponent, state: ComponentState, verbose: bool = False): + def __init__(self, component: Component, state: ComponentState, verbose: bool = False): self._state: ComponentState = state - self._component: BaseComponent = component + self._component: Component = component self._verbose: bool = verbose def __call__(self, state: Optional[ComponentState] = None, msg: Optional[str] = None) -> ComponentState: @@ -88,7 +101,7 @@ def verbose(self, value: bool) -> None: self._verbose = value -class BaseComponent: +class Component(base_class): """Base class to define a Limbus Component. Args: @@ -112,7 +125,7 @@ def __init__(self, name: str): # Last execution to be run in the __call__ loop. self._stopping_iteration: int = 0 # 0 means run forever - def init_from_component(self, ref_component: BaseComponent) -> None: + def init_from_component(self, ref_component: Component) -> None: """Init basic execution params from another component. Args: @@ -313,7 +326,7 @@ async def _run_with_hooks(self, *args, **kwargs) -> bool: if len(self._inputs) == 0: # RUNNING state is set once the input params are received, if there are not inputs the state is set here self.set_state(ComponentState.RUNNING) - self.set_state(await self._run_forward(*args, **kwargs)) + self.set_state(await self._run_forward(*args, **kwargs)) except ComponentStoppedError as e: self.set_state(e.state) except Exception as e: @@ -330,6 +343,9 @@ async def _run_with_hooks(self, *args, **kwargs) -> bool: return True async def _run_forward(self, *args, **kwargs) -> ComponentState: + if nn.Module in Component.__mro__: + # If the component inherits from nn.Module, the forward method is called by the __call__ method + return await nn.Module.__call__(self, *args, **kwargs) return await self.forward(*args, **kwargs) @abstractmethod @@ -369,49 +385,3 @@ def init_pipeline(self) -> None: """ pass - - -try: - class TorchComponent(BaseComponent, nn.Module): - """Base class for all the components that inherit from nn.Module.""" - def __init__(self, name: str): - BaseComponent.__init__(self, name) - nn.Module.__init__(self) - - async def _run_forward(self, *args, **kwargs) -> ComponentState: - # If the component inherits from nn.Module, the forward method is called by the __call__ method - return await nn.Module.__call__(self, *args, **kwargs) -except NameError: - pass - -def set_component_base_class(base_class: Type = object): # -> Type[Union[TorchComponent, BaseComponent]]: - """Get a component class inheriting from a given base class. - - At this moment only nn.Module and object are supported. - When limbus module is imported calling this method the base class of the component can be changed. - E.g.: `limbus.Componet = limbus.get_component_class()`. - - Args: - base_class: Base class of the component. - - Returns: - Component class. - - """ - try: - if base_class == nn.Module: - return TorchComponent - except: - pass - if base_class == object: - return BaseComponent - raise ValueError(f"Base class {base_class} not supported. Must be nn.Module or object.") - -# By default the component inherits from nn.Module if it is available. -# To change this behaviour when limbus is imported add the next lines: -# from limbus import core -# core.Component = core.set_component_base_class() -try: - Component = set_component_base_class(nn.Module) -except: - Component = set_component_base_class() From 1813ae4ab338cd77987d7e6bbb06ee951fe20dac Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Fri, 10 Mar 2023 23:42:36 +0100 Subject: [PATCH 12/18] minor improvemetn + address test --- limbus/core/component.py | 14 +++++++------- tests/core/test_pipeline.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/limbus/core/component.py b/limbus/core/component.py index 6a64b1d..7934317 100644 --- a/limbus/core/component.py +++ b/limbus/core/component.py @@ -1,7 +1,7 @@ """Component definition.""" from __future__ import annotations from abc import abstractmethod -from typing import List, Optional, TYPE_CHECKING, Callable, Type, Union +from typing import List, Optional, TYPE_CHECKING, Callable, Type, Union, Any, Coroutine import logging import asyncio import traceback @@ -125,6 +125,12 @@ def __init__(self, name: str): # Last execution to be run in the __call__ loop. self._stopping_iteration: int = 0 # 0 means run forever + # method called in _run_with_hooks to execute the component forward method + self._run_forward: Callable[..., Coroutine[Any, Any, ComponentState]] = self.forward + if nn.Module in Component.__mro__: + # If the component inherits from nn.Module, the forward method is called by the __call__ method + self._run_forward = nn.Module.__call__ + def init_from_component(self, ref_component: Component) -> None: """Init basic execution params from another component. @@ -342,12 +348,6 @@ async def _run_with_hooks(self, *args, **kwargs) -> bool: # if there is not a pipeline, the component is executed only once return True - async def _run_forward(self, *args, **kwargs) -> ComponentState: - if nn.Module in Component.__mro__: - # If the component inherits from nn.Module, the forward method is called by the __call__ method - return await nn.Module.__call__(self, *args, **kwargs) - return await self.forward(*args, **kwargs) - @abstractmethod async def forward(self, *args, **kwargs) -> ComponentState: """Run the component, this method shouldn't be called, instead call __call__.""" diff --git a/tests/core/test_pipeline.py b/tests/core/test_pipeline.py index 3170c5a..51fe2dc 100644 --- a/tests/core/test_pipeline.py +++ b/tests/core/test_pipeline.py @@ -56,9 +56,9 @@ def test_pipeline_iterable(self): c2 = Constant("c2", 0) unbind = Unbind("unbind") show0 = Printer("print0") - c1.outputs.out.connect(unbind.inputs.tensor) + c1.outputs.out.connect(unbind.inputs.input) c2.outputs.out.connect(unbind.inputs.dim) - unbind.outputs.tensors.select(0).connect(show0.inputs.inp) + unbind.outputs.out.select(0).connect(show0.inputs.inp) pipeline = Pipeline() pipeline.add_nodes([c1, c2, unbind, show0]) out = pipeline.run(1) From 21a4925249f8ab99f2b19e7b63203b62fbd61cfc Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Sun, 12 Mar 2023 01:16:49 +0100 Subject: [PATCH 13/18] move config to another module --- examples/defining_cmps.py | 2 +- limbus/__init__.py | 2 ++ limbus/core/component.py | 2 +- limbus_config/__init__.py | 0 {limbus => limbus_config}/config.py | 2 +- setup.cfg | 2 +- 6 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 limbus_config/__init__.py rename {limbus => limbus_config}/config.py (84%) diff --git a/examples/defining_cmps.py b/examples/defining_cmps.py index 8b66561..006b120 100644 --- a/examples/defining_cmps.py +++ b/examples/defining_cmps.py @@ -3,7 +3,7 @@ import asyncio # If you want to change the limbus config you need to do it before importing any limbus module!!! -import limbus.config as config +from limbus_config import config config.COMPONENT_TYPE = "torch" from limbus.core import Component, Params, InputParams, OutputParams, ComponentState, VerboseMode # noqa: E402 diff --git a/limbus/__init__.py b/limbus/__init__.py index e69de29..9845c8b 100644 --- a/limbus/__init__.py +++ b/limbus/__init__.py @@ -0,0 +1,2 @@ +from limbus import widgets +from limbus.core import Pipeline, Component diff --git a/limbus/core/component.py b/limbus/core/component.py index 7934317..fa3c9a4 100644 --- a/limbus/core/component.py +++ b/limbus/core/component.py @@ -12,7 +12,7 @@ except ImportError: pass -from limbus import config +from limbus_config import config from limbus.core.params import Params, InputParams, OutputParams from limbus.core.states import ComponentState, ComponentStoppedError # Note that Pipeline class cannot be imported to avoid circular dependencies. diff --git a/limbus_config/__init__.py b/limbus_config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/limbus/config.py b/limbus_config/config.py similarity index 84% rename from limbus/config.py rename to limbus_config/config.py index 1e3e29d..a58a5c0 100644 --- a/limbus/config.py +++ b/limbus_config/config.py @@ -2,7 +2,7 @@ Usage: Before importing any other Limbus module, set the COMPONENT_TYPE variable as: - > import limbus.config as config + > from limbus_config import config > config.COMPONENT_TYPE = "torch" diff --git a/setup.cfg b/setup.cfg index 2f05d8c..50abe76 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ ignore = exclude = docs/src [mypy] -files = limbus, tests +files = examples, limbus, tests, limbus_config show_error_codes = True ignore_missing_imports = True From 0f66ccd0956f077c1722b3f3e823a3bfb15eab9f Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Sun, 12 Mar 2023 01:39:49 +0100 Subject: [PATCH 14/18] added tests for the config --- tests/config/test_config.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/config/test_config.py diff --git a/tests/config/test_config.py b/tests/config/test_config.py new file mode 100644 index 0000000..aef4e9a --- /dev/null +++ b/tests/config/test_config.py @@ -0,0 +1,31 @@ +"""Config tests.""" +import sys + +from torch import nn + + +def remove_limbus_imports(): + """Remove limbus dependencies from sys.modules.""" + for key in list(sys.modules.keys()): + if key.startswith("limbus"): + del sys.modules[key] + + +def test_torch_base_class(): + remove_limbus_imports() + from limbus_config import config + config.COMPONENT_TYPE = "torch" + import limbus + mro = limbus.Component.__mro__ + remove_limbus_imports() + assert len(mro) == 3 + assert nn.Module in mro + + +def test_generic_base_class(): + remove_limbus_imports() + import limbus + mro = limbus.Component.__mro__ + remove_limbus_imports() + assert len(mro) == 2 + assert nn.Module not in mro From e664852958e650df39b0b041eed15c783a35f747 Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Sun, 12 Mar 2023 02:04:03 +0100 Subject: [PATCH 15/18] add doc --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index dba858c..10d946a 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,15 @@ class Add(Component): return ComponentState.OK ``` +**Note** that `Component` can inherint from `nn.Module`. By default inherints from `object`. + +To change the inheritance, before importing any other `limbus` module, set the `COMPONENT_TYPE` variable as: + +```python +from limbus_config import config +config.COMPONENT_TYPE = "torch" +``` + ## Ecosystem Limbus is a core technology to easily build different components and create generic pipelines. In the following list, you can find different examples From 5a37f0866e17ad6e5739cc41f50286ae5aee0622 Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Sun, 12 Mar 2023 02:14:31 +0100 Subject: [PATCH 16/18] address test --- examples/defining_cmps.py | 1 + tests/core/test_pipeline.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/defining_cmps.py b/examples/defining_cmps.py index 7044267..b8e5b0b 100644 --- a/examples/defining_cmps.py +++ b/examples/defining_cmps.py @@ -9,6 +9,7 @@ from limbus.core import Component, InputParams, OutputParams, ComponentState, OutputParam, InputParam # noqa: E402 from limbus.core import Pipeline, VerboseMode # noqa: E402 + # define the components # --------------------- class Add(Component): diff --git a/tests/core/test_pipeline.py b/tests/core/test_pipeline.py index 51fe2dc..3170c5a 100644 --- a/tests/core/test_pipeline.py +++ b/tests/core/test_pipeline.py @@ -56,9 +56,9 @@ def test_pipeline_iterable(self): c2 = Constant("c2", 0) unbind = Unbind("unbind") show0 = Printer("print0") - c1.outputs.out.connect(unbind.inputs.input) + c1.outputs.out.connect(unbind.inputs.tensor) c2.outputs.out.connect(unbind.inputs.dim) - unbind.outputs.out.select(0).connect(show0.inputs.inp) + unbind.outputs.tensors.select(0).connect(show0.inputs.inp) pipeline = Pipeline() pipeline.add_nodes([c1, c2, unbind, show0]) out = pipeline.run(1) From 911a15817bb0527b0504e711269fa7f9d0df8e80 Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Mon, 13 Mar 2023 00:53:26 +0100 Subject: [PATCH 17/18] bug when torch not installed --- limbus/core/component.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/limbus/core/component.py b/limbus/core/component.py index fa3c9a4..d01d381 100644 --- a/limbus/core/component.py +++ b/limbus/core/component.py @@ -22,15 +22,15 @@ log = logging.getLogger(__name__) +base_class: Type = object if config.COMPONENT_TYPE == "generic": - base_class: Type = object + pass elif config.COMPONENT_TYPE == "torch": try: base_class = nn.Module except NameError: log.error("Torch not installed. Using generic base class.") else: - base_class = object log.error("Invalid component type. Using generic base class.") From 5d6c99b6473887b8ab301dea539c328b53a77f83 Mon Sep 17 00:00:00 2001 From: Luis Ferraz Date: Mon, 13 Mar 2023 23:31:41 +0100 Subject: [PATCH 18/18] correct readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 10d946a..c9ce650 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ Let's see a very simple example that sums 2 integers: class Add(Component): """Add two numbers.""" # NOTE: type definition is optional, but it helps with the intellisense. ;) - class InputsTyping(OutputParams): + class InputsTyping(InputParams): a: InputParam b: InputParam