From 75c5d29dc8ec2d93f36ae1f2b9192fe811d6a4ca Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 17 Aug 2026 08:21:12 +0200 Subject: [PATCH 01/10] minor logging --- src/detectmatelibrary/common/deeplearning_detector.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/detectmatelibrary/common/deeplearning_detector.py b/src/detectmatelibrary/common/deeplearning_detector.py index 1189483e..25a88b58 100644 --- a/src/detectmatelibrary/common/deeplearning_detector.py +++ b/src/detectmatelibrary/common/deeplearning_detector.py @@ -7,6 +7,7 @@ from detectmatelibrary import schemas from typing import Any +import logging class DeepLearningDetectorConfig(CoreDetectorConfig): @@ -72,8 +73,8 @@ def post_train(self) -> None: if "top_k" in self.stats: self.top_k = int(self.stats["top_k"]) - print(self.model) - print("Top k assigned", self.top_k) + logging.info(self.model) + logging.info(f"Top k assigned {self.top_k}") def detect( self, From 8cdc84c7612d849c9119832d2d619a16c67ac998 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 17 Aug 2026 08:27:39 +0200 Subject: [PATCH 02/10] move component to _basic_component --- .../common/_core_op/_basic_component.py | 55 ++++++++++++++++++ src/detectmatelibrary/common/core.py | 57 +------------------ 2 files changed, 58 insertions(+), 54 deletions(-) create mode 100644 src/detectmatelibrary/common/_core_op/_basic_component.py diff --git a/src/detectmatelibrary/common/_core_op/_basic_component.py b/src/detectmatelibrary/common/_core_op/_basic_component.py new file mode 100644 index 00000000..9da8f740 --- /dev/null +++ b/src/detectmatelibrary/common/_core_op/_basic_component.py @@ -0,0 +1,55 @@ +from detectmatelibrary.common.core import CoreConfig +from detectmatelibrary.schemas import BaseSchema +from detectmatelibrary.utils.persistency.component_interfaces import Stoppable + + +from typing import Any, Dict, List + + +class Component: + """Empty methods.""" + def __init__( + self, + name: str, + type_: str = "Core", + config: CoreConfig = CoreConfig(), + ) -> None: + self.name, self.type_, self.config = name, type_, config + self.saver: Stoppable | None = None + + def __repr__(self) -> str: + return f"<{self.type_}> {self.name}: {self.config}" + + def run( + self, input_: List[BaseSchema] | BaseSchema, output_: BaseSchema + ) -> bool: + return False + + def train( + self, input_: List[BaseSchema] | BaseSchema, + ) -> None: + pass + + def configure( + self, input_: List[BaseSchema] | BaseSchema, + ) -> None: + pass + + def set_configuration(self) -> None: + pass + + def post_train(self) -> None: + pass + + def get_config(self) -> Dict[str, Any]: + return self.config.get_config() + + def update_config(self, new_config: Dict[str, Any]) -> None: + self.config.update_config(new_config) + + def __enter__(self) -> "Component": + return self + + def __exit__(self, *_: Any) -> None: + if self.saver is not None: + self.saver.stop() diff --git a/src/detectmatelibrary/common/core.py b/src/detectmatelibrary/common/core.py index 7d52d54c..151cf45c 100644 --- a/src/detectmatelibrary/common/core.py +++ b/src/detectmatelibrary/common/core.py @@ -1,3 +1,4 @@ +from detectmatelibrary.common._core_op._basic_component import Component from detectmatelibrary.common._core_op._fit_logic import FitLogicState, StatesL from detectmatelibrary.common._core_op._schema_pipeline import SchemaPipeline from detectmatelibrary.common._core_op._fit_logic import FitLogic @@ -12,10 +13,9 @@ from detectmatelibrary.tools.logging import logger, setup_logging -from typing import Any, Dict, List +from typing import Any from detectmatelibrary.utils.persistency.component_interfaces import PersistencyOp -from detectmatelibrary.utils.persistency.component_interfaces import Stoppable setup_logging() @@ -43,7 +43,7 @@ def __iter__(self) -> "TrainBuffer": return self -# Core component skeleton structure ################################################ +# Core component ################################################ class CoreConfig(BasicConfig): start_id: int = 10 @@ -52,57 +52,6 @@ class CoreConfig(BasicConfig): use_config_data_as_training: bool = True -class Component: - """Empty methods.""" - def __init__( - self, - name: str, - type_: str = "Core", - config: CoreConfig = CoreConfig(), - ) -> None: - self.name, self.type_, self.config = name, type_, config - self.saver: Stoppable | None = None - - def __repr__(self) -> str: - return f"<{self.type_}> {self.name}: {self.config}" - - def run( - self, input_: List[BaseSchema] | BaseSchema, output_: BaseSchema - ) -> bool: - return False - - def train( - self, input_: List[BaseSchema] | BaseSchema, - ) -> None: - pass - - def configure( - self, input_: List[BaseSchema] | BaseSchema, - ) -> None: - pass - - def set_configuration(self) -> None: - pass - - def post_train(self) -> None: - pass - - def get_config(self) -> Dict[str, Any]: - return self.config.get_config() - - def update_config(self, new_config: Dict[str, Any]) -> None: - self.config.update_config(new_config) - - def __enter__(self) -> "Component": - return self - - def __exit__(self, *_: Any) -> None: - if self.saver is not None: - self.saver.stop() - - -# Core component ################################################ - class CoreComponent(Component): """Base class for all components in the system.""" def __init__( From 39c5c12972203d21208b0f25e2deab5f59ab71ae Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 17 Aug 2026 09:19:51 +0200 Subject: [PATCH 03/10] start integrating prototype code into production --- .../common/_core_op/_basic_component.py | 6 +- .../common/_core_op/_fed_component.py | 100 ++++++++++++++++++ src/detectmatelibrary/common/core.py | 9 +- 3 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 src/detectmatelibrary/common/_core_op/_fed_component.py diff --git a/src/detectmatelibrary/common/_core_op/_basic_component.py b/src/detectmatelibrary/common/_core_op/_basic_component.py index 9da8f740..143a8e99 100644 --- a/src/detectmatelibrary/common/_core_op/_basic_component.py +++ b/src/detectmatelibrary/common/_core_op/_basic_component.py @@ -1,7 +1,7 @@ -from detectmatelibrary.common.core import CoreConfig -from detectmatelibrary.schemas import BaseSchema from detectmatelibrary.utils.persistency.component_interfaces import Stoppable +from detectmatelibrary.schemas import BaseSchema +from detectmatelibrary.common._config import BasicConfig from typing import Any, Dict, List @@ -12,7 +12,7 @@ def __init__( self, name: str, type_: str = "Core", - config: CoreConfig = CoreConfig(), + config: BasicConfig = BasicConfig(), ) -> None: self.name, self.type_, self.config = name, type_, config self.saver: Stoppable | None = None diff --git a/src/detectmatelibrary/common/_core_op/_fed_component.py b/src/detectmatelibrary/common/_core_op/_fed_component.py new file mode 100644 index 00000000..04c40be1 --- /dev/null +++ b/src/detectmatelibrary/common/_core_op/_fed_component.py @@ -0,0 +1,100 @@ + +import warnings +from typing import Self, overload + + +class IncompabtibleFed(Exception): + def __init__(self) -> None: + super().__init__("Instances are incompatible") + + +class _CompOp: + @staticmethod + def is_compatible(main_inst: object, other_inst: object) -> None: + if not isinstance(other_inst, type(main_inst)): + raise IncompabtibleFed() + + @staticmethod + def reset(main_inst: object, attr: str) -> None: + main_inst.__setattr__(attr, {main_inst}) + + @staticmethod + def combine(main_inst: object, attr: str, other_inst: object) -> None: + _CompOp.is_compatible(main_inst, other_inst) + set_main: set[object] = getattr(main_inst, attr) + + set_main.update(getattr(other_inst, attr)) + for elem in set_main: + getattr(elem, attr).update(set_main) + + @staticmethod + def uncombine(main_inst: object, attr: str, other_inst: object) -> None: + _CompOp.is_compatible(main_inst, other_inst) + set_main: set[object] = getattr(main_inst, attr) + + for elem in list(set_main): + if elem == other_inst: + _CompOp.reset(other_inst, attr) + else: + elem._components.remove(other_inst) # type: ignore + + @staticmethod + def stack(main_inst: object, attr: str, list_other_inst: list[object]) -> None: + set_main: set[object] = getattr(main_inst, attr) + for other_inst in list_other_inst: + _CompOp.is_compatible(main_inst, other_inst) + set_main.add(other_inst) + + +class FedOperations: + __COMPONENT: str = "_components" + + def __init__(self) -> None: + self._components: set["FedOperations"] + _CompOp.reset(self, self.__COMPONENT) + + def __add__(self, other: object) -> Self: + _CompOp.combine(self, attr=self.__COMPONENT, other_inst=other) + + return self + + def __sub__(self, other: object) -> Self: + _CompOp.uncombine(self, attr=self.__COMPONENT, other_inst=other) + + return self + + @overload + def stack(self, other: bytes | list[bytes]) -> None: + pass + + @overload + def stack(self, other: object | list[object]) -> None: + pass + + def stack(self, other: object | list[object | bytes] | bytes) -> None: + if not isinstance(other, list): + other = [other] + _CompOp.stack( + self, attr=self.__COMPONENT, list_other_inst=[ + self.from_binary(inst) if isinstance(inst, bytes) else inst for inst in other + ] + ) + + def aggregate(self, unstack: bool = False) -> None | bytes: + self.aggregate_strategy(self._components) + + if unstack: + _CompOp.reset(self, self.__COMPONENT) + + return self.to_binary() + + def to_binary(self) -> bytes | None: + warnings.warn("To binary not implemented, return None") + return None + + def from_binary(self, binary: bytes) -> object: + warnings.warn(f"To binary not implemented, return None for {binary!r}") + return None + + def aggregate_strategy(self, components: set["FedOperations"]) -> None: + warnings.warn(f"No strategy found, aggregations does nothing for {components}") diff --git a/src/detectmatelibrary/common/core.py b/src/detectmatelibrary/common/core.py index 151cf45c..75a3c645 100644 --- a/src/detectmatelibrary/common/core.py +++ b/src/detectmatelibrary/common/core.py @@ -1,6 +1,7 @@ -from detectmatelibrary.common._core_op._basic_component import Component from detectmatelibrary.common._core_op._fit_logic import FitLogicState, StatesL from detectmatelibrary.common._core_op._schema_pipeline import SchemaPipeline +from detectmatelibrary.common._core_op._fed_component import FedOperations +from detectmatelibrary.common._core_op._basic_component import Component from detectmatelibrary.common._core_op._fit_logic import FitLogic from detectmatelibrary.utils.data_buffer import DataBuffer, ArgsBuffer, BufferMode @@ -52,7 +53,7 @@ class CoreConfig(BasicConfig): use_config_data_as_training: bool = True -class CoreComponent(Component): +class CoreComponent(Component, FedOperations): """Base class for all components in the system.""" def __init__( self, @@ -63,7 +64,9 @@ def __init__( input_schema: type[BaseSchema] = BaseSchema, output_schema: type[BaseSchema] = BaseSchema ) -> None: - super().__init__(name=name, type_=type_, config=config) + Component.__init__(self, name=name, type_=type_, config=config) + FedOperations.__init__(self) + self.config: CoreConfig self.input_schema, self.output_schema = input_schema, output_schema self.data_buffer = DataBuffer(args_buffer) From 67c3f18e1ec842f43fc7ed9b770a7429bde3a78e Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 17 Aug 2026 09:37:06 +0200 Subject: [PATCH 04/10] add initial tests --- tests/test_common/test_core_federation.py | 135 ++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 tests/test_common/test_core_federation.py diff --git a/tests/test_common/test_core_federation.py b/tests/test_common/test_core_federation.py new file mode 100644 index 00000000..f2b93032 --- /dev/null +++ b/tests/test_common/test_core_federation.py @@ -0,0 +1,135 @@ +from detectmatelibrary.common._core_op._fed_component import IncompabtibleFed +from detectmatelibrary.common.core import CoreComponent + +import struct + +import pytest + + +class DummyComponent(CoreComponent): + pass + + +class DummyComponent2(CoreComponent): + pass + + +class TestJoinOp: + def test_add(self) -> None: + component1 = CoreComponent(name="comp_1") + component2 = CoreComponent(name="comp_2") + component3 = CoreComponent(name="comp_3") + + component2 + component3 + assert len(component2._components) == 2 + assert component3._components == component2._components + + component1 = component1 + component3 + print(component2._components) + assert len(component1._components) == 3 + assert component1._components == component2._components + assert component1._components == component3._components + + def test_sub(self) -> None: + component1 = CoreComponent(name="comp_1") + component2 = CoreComponent(name="comp_2") + component3 = CoreComponent(name="comp_3") + + (component1 + component2 + component3) - component3 + assert len(component2._components) == 2 + assert component1._components == component2._components + assert component3._components == {component3} + + def test_incompatible(self) -> None: + component1 = DummyComponent2(name="comp_1") + component2 = DummyComponent(name="comp_2") + + with pytest.raises(IncompabtibleFed): + component1 + component2 + with pytest.raises(IncompabtibleFed): + component1 - component2 + + def test_stack(self) -> None: + component1 = CoreComponent(name="comp_1") + component2 = CoreComponent(name="comp_2") + component3 = CoreComponent(name="comp_3") + component4 = CoreComponent(name="comp_4") + + component1.stack(component2) + assert len(component1._components) == 2 + assert component1._components != component2._components + + component1.stack([component3, component4]) + assert len(component1._components) == 4 + + +class DummyAppendList(CoreComponent): + def __init__( + self, elems: list[str], name: str = "test", *args, **kwargs + ) -> None: + super().__init__(name, *args, **kwargs) + self.elems = elems + + def aggregate_strategy(self, components): + final_list = [] + for component in components: + final_list.extend(component.elems) + + final_list = list(set(final_list)) + for component in components: + component.elems = final_list + + def to_binary(self): + return struct.pack(f">{len(self.elems)}h", *self.elems) + + def from_binary(self, binary): + num_ints = len(binary) // 2 + elems = list(struct.unpack(f">{num_ints}h", binary)) + return DummyAppendList(elems=elems) + + +class TestFedComponent: + def test_basic_aggregation(self) -> None: + comp1 = DummyAppendList(elems=[1, 2]) + comp2 = DummyAppendList(elems=[3, 4]) + + comp1.aggregate() + assert set(comp1.elems) == {1, 2} + + (comp1 + comp2).aggregate() + assert set(comp1.elems) == {1, 2, 3, 4} + + def test_stack_basic_aggregation(self) -> None: + comp1 = DummyAppendList(elems=[1, 2]) + comp2 = DummyAppendList(elems=[3, 4]) + comp3 = DummyAppendList(elems=[5]) + + comp1.stack([comp2, comp3]) + comp1.aggregate(unstack=True) + assert set(comp1.elems) == {1, 2, 3, 4, 5} + assert len(comp1._components) == 1 + + comp1.stack([comp2, comp3]) + comp1.aggregate(unstack=False) + assert set(comp1.elems) == {1, 2, 3, 4, 5} + assert len(comp1._components) == 3 + + def test_sanity_check(self) -> None: + comp1 = DummyAppendList(elems=[1, 2]) + comp2 = comp1.from_binary(comp1.to_binary()) + + assert comp2.elems == [1, 2] + + def test_stack_binary_aggregation(self) -> None: + comp1 = DummyAppendList(elems=[1, 2]) + comp2 = DummyAppendList(elems=[3, 4]) + comp3 = DummyAppendList(elems=[5]) + + comp1.stack([comp2.to_binary(), comp3.to_binary()]) + comp1.aggregate(unstack=True) + assert set(comp1.elems) == {1, 2, 3, 4, 5} + + comp1.stack([comp2.to_binary(), comp3.to_binary()]) + output = comp1.aggregate(unstack=True) + assert set(comp1.elems) == {1, 2, 3, 4, 5} + assert isinstance(output, bytes) From c469bbadfaa423ce9cb823e3357d866668a1b2b4 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 17 Aug 2026 09:46:21 +0200 Subject: [PATCH 05/10] add warning tests --- tests/test_common/test_core_federation.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/test_common/test_core_federation.py b/tests/test_common/test_core_federation.py index f2b93032..66627033 100644 --- a/tests/test_common/test_core_federation.py +++ b/tests/test_common/test_core_federation.py @@ -25,7 +25,6 @@ def test_add(self) -> None: assert component3._components == component2._components component1 = component1 + component3 - print(component2._components) assert len(component1._components) == 3 assert component1._components == component2._components assert component1._components == component3._components @@ -88,6 +87,14 @@ def from_binary(self, binary): return DummyAppendList(elems=elems) +class DummyAppendListEmpty(CoreComponent): + def __init__( + self, elems: list[str], name: str = "test", *args, **kwargs + ) -> None: + super().__init__(name, *args, **kwargs) + self.elems = elems + + class TestFedComponent: def test_basic_aggregation(self) -> None: comp1 = DummyAppendList(elems=[1, 2]) @@ -133,3 +140,17 @@ def test_stack_binary_aggregation(self) -> None: output = comp1.aggregate(unstack=True) assert set(comp1.elems) == {1, 2, 3, 4, 5} assert isinstance(output, bytes) + + def test_empty_feed_fields(self) -> None: + comp1 = DummyAppendListEmpty(elems=[1, 2]) + comp2 = DummyAppendListEmpty(elems=[3, 4]) + comp3 = DummyAppendListEmpty(elems=[5]) + + with pytest.warns(UserWarning): + comp1.to_binary() + + with pytest.warns(UserWarning): + comp1.from_binary(b"") + + with pytest.warns(UserWarning): + comp1.aggregate_strategy({comp1, comp2, comp3}) From 6c5a0e4fe0d09cd46f84de2941f3436338782a16 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 17 Aug 2026 10:04:23 +0200 Subject: [PATCH 06/10] update overall architecture --- docs/overall_architecture.md | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/overall_architecture.md b/docs/overall_architecture.md index 49f8e821..77850f6e 100644 --- a/docs/overall_architecture.md +++ b/docs/overall_architecture.md @@ -70,14 +70,42 @@ class Component(CoreComponent): * Default: the component is just processing data """ + def export_state( + self, path: str | None = None, storage_options: dict[str, Any] | None = None, + ) -> bytes | None: + """Export the current state if persistency class was implemented""" + + def import_state( + self, path: str | bytes, storage_options: dict[str, Any] | None = None + ) -> None: + """Import the current state if persistency class was implemented""" + def process(self, data: BaseSchema | bytes) -> BaseSchema | bytes | None: """Process the data in a stream fashion (Defined in the CoreComponent)""" def get_config(self) -> Dict[str, Any]: - """"Get the configuration of the component (Defined in the CoreComponent)""" + """Get the configuration of the component (Defined in the CoreComponent)""" def update_config(self, new_config: Dict[str, Any]) -> None: - """"Update the configuration of the component (Defined in the CoreComponent)""" + """Update the configuration of the component (Defined in the CoreComponent)""" + + def get_window_size(self) -> int: + """Get window size of the data buffer""" + + def stack(self, other: object | list[object | bytes] | bytes) -> None: + """(Federation only) stack multiple components for federation tasks""" + + def aggregate(self, unstack: bool = False) -> None | bytes: + """(Federation only) aggregate multiple components""" + + def to_binary(self) -> bytes | None: + """(Federation only) fill it to be compatible with federation ops""" + + def from_binary(self, binary: bytes) -> object: + """(Federation only) fill it to be compatible with federation ops""" + + def aggregate_strategy(self, components: set["FedOperations"]) -> None: + """(Federation only) fill it to be compatible with federation ops""" ``` Go back [Index](index.md) From 81c60ee334b23579ded19e3d7369be54678460a2 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 17 Aug 2026 12:11:55 +0200 Subject: [PATCH 07/10] first version of docs --- docs/diagrams.drawio | 292 +++++++++++++++++++++++++-------- docs/federation.md | 128 +++++++++++++++ docs/img/fed_combine_first.png | Bin 0 -> 80494 bytes docs/img/fed_stack_later.png | Bin 0 -> 91311 bytes mkdocs.yml | 1 + 5 files changed, 349 insertions(+), 72 deletions(-) create mode 100644 docs/federation.md create mode 100644 docs/img/fed_combine_first.png create mode 100644 docs/img/fed_stack_later.png diff --git a/docs/diagrams.drawio b/docs/diagrams.drawio index 61c4bf68..3444a1ca 100644 --- a/docs/diagrams.drawio +++ b/docs/diagrams.drawio @@ -1,108 +1,154 @@ - - + + - - + + - - + + - - + + - - + + - - + + - + + + + + + + - - - - - - + + - + - - - - - - + + - - + + + + + - - + + - - + + - + + + + - - + + - + - - + + - + - - + + - - + + + + + - - + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - + - - - - - - + + - + - - - - - - + + + + + + + + + + + + + + - + @@ -215,25 +261,25 @@ - + - + - + - + - + @@ -242,8 +288,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -280,16 +428,16 @@ - + - + - + diff --git a/docs/federation.md b/docs/federation.md new file mode 100644 index 00000000..dcc5e069 --- /dev/null +++ b/docs/federation.md @@ -0,0 +1,128 @@ +# Federation + +In this section, we will explain how to use the federation setup. For a component to use federation needs to have implemented the next methods. + +```python + def to_binary(self) -> bytes | None: + """(Federation only) fill it to be compatible with federation ops""" + + def from_binary(self, binary: bytes) -> object: + """(Federation only) fill it to be compatible with federation ops""" + + def aggregate_strategy(self, components: set["FedOperations"]) -> None: + """(Federation only) fill it to be compatible with federation ops""" +``` + +There are two man ways to use federation: + +* **Combine first**: only can be use when all components run locally. The main idea is to simplify the process by allowing components to share memory. +* **Stack later**: a more standard approach to federated where the "weights" of each component are combine at the end. + +## Example class + +For all the examplaes bellow, we will use this code: + +```python +import struct + + +class NewComponent(CoreComponent): # Inherent from CoreComponent + def __init__(self, elems): + self.elems = elems + super().__init__() + + def aggregate_strategy(self, components): + final_list = [] + for component in components: + final_list.extend(component.elems) + + final_list = list(set(final_list)) + for component in components: + component.elems = final_list + + def to_binary(self): + return struct.pack(f">{len(self.elems)}h", *self.elems) + + def from_binary(self, binary): + num_ints = len(binary) // 2 + elems = list(struct.unpack(f">{num_ints}h", binary)) + return NewComponent(elems=elems) +``` + + +## Combine first + +The diagram bellow show the workflow: + +![combine](img/fed_combine_first.png) + + +Example 1: +```python +detector1 = NewComponent([1, 2, 3]) +detector2 = NewComponent([4, 5]) +detector3 = NewComponent([6]) + +detector1 + detector2 + detector3 + +detector2.aggregate() # Detector 2 is used as centralize node + +print("Dectector 3", detector3.elems) # All detectors have been updated +``` + +Example 2: + +```python +detector1 = NewComponent([1, 2, 3]) +detector2 = NewComponent([4, 5]) +detector3 = NewComponent([6]) + +(detector1 + detector2 + detector3) - detector2 + +detector2.aggregate() # Detector 2 is used as centralize node +print("Detector 3", detector3.elems) # Still the same + +detector1.aggregate() # Detector 1 is used as centralize node +print("Detector 3", detector3.elems) # Detector 3 has been updated now +``` + +## Stack + +The diagram bellow show the workflow: + +![combine](img/fed_stack_later.png) + +Example 1: +```python +detector1 = NewComponent([1, 2, 3]) +detector2 = NewComponent([4, 5]) +detector3 = NewComponent([6]) + +detector1.stack([detector2, detector3]) + +detector2.aggregate() +print("Detector 3", detector3.elems) # It is not longer combine but stack, so it will not work + +detector1.aggregate() +print("Detector 3", detector3.elems) # Now it will work +``` + +Example 2: +```python +detector1 = NewComponent([1, 2, 3]) + +binary2 = NewComponent([4, 5]).to_binary() +binary3 = (detector3 := NewComponent([6])).to_binary() +print("Binary of Detector 3", binary3) + +detector1.stack([binary2, binary3]) +output = detector1.aggregate(unstack=True) # unstack = True will free memory + +print("Detector 1", detector1.elems) # Detector 1 has been updated +print("Output binary", output) # Output that we send to other componets + +# We update detector 3 now +print("Detector 3", detector3.elems) # Now detector 3 is not in the share memory so it will not work +detector3 = detector3.from_binary(output) +print("Detector 3", detector3.elems) # Now it will work +``` diff --git a/docs/img/fed_combine_first.png b/docs/img/fed_combine_first.png new file mode 100644 index 0000000000000000000000000000000000000000..4a923e097f2f42c2eba90409e409eb9a7157246b GIT binary patch literal 80494 zcmeEv1zc3w`#y|=gtU}`bV+x2Hz)6-EUz}tz3fq_AwuBNmX0|SD@ zz`&Hi*$R#rCl2>wU{uF>Dw}w^_}V!-+hVW?DlY%VBB;#5C(k0lhx!pzv~jewceiu_ ze>mE(2r97%2pFsR@8Q$dciXGzCM2XLY-DO6q6Q9wGh8iQY?sebv9+;vxAb&`xq`#& ziZB-|M^{_$tDU2}hv#n8*=t8ZkLBX4>TU^l(1zLAI)jsKe3viAFC+vegBrlbZ~2I* zDF5;Sdw2A#)*R4x^k10<|H}95y&P?9Jyx!^{7p|7%-Pcs{`Em?n5(O;wdYqSTDrT# ze7-)-4(7bF7AvE|E$z`GE?*sWK;PQZ`P-w$jy9eS%L9lBuS^ehwwkS@y#sn=e!i7Y zTrANiuN?4ju(W~sEPsFL(6{S|UQKrx3|w!GFqc1A`(KpI6>XhSi;Z5#m2p@ERQ~>V zS69Q`)^+7p{x6pk@KA9uwbQisIArg9P~1yFTXC-r_sSUFmd;))i_0P);tWPpumZo> zqkaME28X$#rXauvPG9~M+|cR?%QEut^h3+X9p>d~V~dh0PHqvr;9WAiy!>D{B3VrcTZcif}(}~<-V7u`R9v|mJ`r=TbPTjr@J4R@5&WLR@QAr z-v!YI;PXX$_)*_S8HK|d%|U<9az$b7SFemZ^LvB}&RNlkKcD>usv0zz=$hJc?#e-3^_JJz*v9DQBjGL|pBeAb%7f9!FA??jEf#;sa9JKDPf z7i$e1x2-$)4CP@RK@62&`M|}|#s=lEH}ub6d+?Pt`Kc}%?UGkiNI-PO9j{d(@eN$G z;L5RI$3+Woph760{Y6m(07d{<>5mqFtrR`r%K*yG(H9j^6#%$&R)jgjfJ6a!6y#g_ z2OR$=M8waxV%94%5)v0(9(JvaHV_dSHvT#y68UXK#J?hvO)sLA`}hjz{2o>$_;s+{ zyjCQ(ffX&IO%J#wDpUR9@z9}fU1;PN`-9Mk4tlE*Pu||%-PYdH6J)&`0ov7L%Oj&B z=Q@S?I;#CeUg8e(T+X-!l|bD2XFROPx+u7ssju;{0%&*oYbPq=f2bV)XWXO&1G?o0 z^KxHOF)us2H9GM%a#|<c1NA@j^}h>FFVD@! z(Hneg37ez8{fE;dKZfa%f!VIdS9wsb%V#ap3*&hlTVwOjp5&(_@$&|9dS zZt38%Z?d)dhB95po(b@ITDsfYf;17uI&VlWE&1#}=c!P>222GIooiUOHL^rIt+k>= zA9J^L1`*X8<#)jhR#tN(r}aHk_%abanB!6+zQXkh3aunyXpKW>lIW2xbODiuXNS=YNW~3`y5{JYk^~7UQ2&yl;m5g%ocIUj9KF1RAJ*X*gLM z{+2#Px4wM+0s82dmI+HCL1RT-D+4iiOKr`)`fv>eAI)Xg_oX4m!_(c%+SAM3_MjJN zBmfX~?M2t%x-ZlDum1(0oPTxyi=sY2b~^SjEpeERgQAd*@&xI zhamqJE3>THaKIFyn|#nb!+N^$TOD84^tJz8mzl5R#OuQwzt~EGx+Zh~^%`I{6xc=y z>9R;xxidwWyR8ynNUS|Une+?gw)*+7p@ic2g*!Vsn#`LLh-h!Z~ZkzYoh{MG*+(ihZeq^!GN>C8Gq*HmeJpzoVOAy z*0B-)L!?-v7igF9|Bu66jYTV7OAyV2{X8JPI>gG50_ash5BWa>aWotSg#k-cPb&I? za1?Ltfl`t05!d=4zmD}@UuO|O`}w~|ZlmYw;=Atpf~ZeWR)=1m|LyuqiNNwge9ujz zvkG+E?Vo>VSM=V-q<9i;HO z=8#p5`K3MNhSJ&y4sReWH23ziq_(*jI3cmGVU-X%bJ~3J|62^)f6_;hJEG_g26P&- zDR{UIt$!mVi^`7I5b2wmhuiR`S6tp&ytCnXzb6kTB>c7STFr3Lz~jfG9g%g!^(N!t zgx7VS{^%rHWSvC*$Ls&PJluxXwW5W;JP)_w&*$O||a&|4tY3_uAk#V0Tt`ZlG$2=yD(*=!{11(m?fEtdYL~2{dvpG zmn31$7DyD31p};a9TnLW)a8Z(Uhx56@dxV)7i$Fk4>8xjh?v*cCpNK|H>ywk+v4W$ z7U$6YB&%`mr*Fyn#vlaO4Y_IK-0xn&+0gfI(6RDo9oN5^{RpkAKMJAk=_leubm#Pn z$*w-V6+PspwV(e_Wr)>B#;kIUrf60J#Y2ACz_9ceYSou%$hE}d9|FeTOy7mkq#HV3 zpg}8I`F?y}w0_9dztKZ(27OoAYveAnN5jn3Pf$f~uZEYtk*($Tf&^9(868=RE6(S<2 zUYE67fQ8T{U$i>^cpLhrRcZY`@L$M+mRc%*{?n1%eZ*806;$lCRdl$04;eZc=$L%- zx-gHW4Tb+b8tK1RgMlWDZ+)nucMt;+ws=YE$Ec!mr1lY$}-joc=>@w|H&BDld!t; z70s!k0l_aVWE<{1st$)#{r2q96NPK42`hUz?cwI%Nc8Kg)!?b_`YZ2M{I<>O58dWk zD7%8$mVwerW5*BMY4F`obVu3UUp-{~|Md8ljWUvTc7*QV`OlJ2mL#-pm%-+6mw&H| z|A#%0=ciWS)>`XIykBdrzbPeyj>Kq3j@}jh^ODKc=a-|0M8^*FkpFWsxs0&|6@Tgz zzWC9V%$4b{c7Xo1XI-A(+W(?*fOXq~e$eB8e%+mge{UDV=I|ZAzoQUc?siKsm_htN;$>Z0zS@BHOJ{88So;s_ z0Q%X*zo8ne&)heO9;~Fw|F9mQQ`O(nA*|0lH;E>! zSN5Z#_ojYn;zVstMM3=4t>haNIafdX^0hzd{rowb&NoO^R|4m6)>104u7z6g=clTx zi1UZ1sw*Vh>Ta(;-yZgVqU88DDqj5CEao?}g>Rc$Ry!KeEXt41$G#nOHDsU%{hz6u zf6^Ylhw%T?$0&f%uqGI6$_VnC*@N)6on=42E9TolH>EvnTowC{LJoSX!B6W)*j#mQ zk#!mN=BVUsEd7n@J8Q|7Rgz_iPWe&P%c?^Cmig9t{aAGtzcd50v3~qL4*&N|!hW+5 zD*DYc)&BuV`R$;qRHyHYhCL>rbwTuV1nM3SED*(6zDq*r@#V$F$9V?&H^RAHwV2-n5zp zpySz(cOL@iQarjN<%jKW`bXTy`W5@neQfMLmMF5Hzp+K#Rou(ah2O*3PLbP2TSe4W zRbk~ZXB)ltd-b*98@)Yo=_Sbjz4{x#8qhSh@*K{8^{c;KwJZg74tl`&w2ckDy&RmJ z47|;_H!A3@>&ZaZ<5xQ`zULj%sFI$KgQKUdKHSn8ydzA>2dE-Aw&snrn~PxLNBi~l ze*Tvx5H^(gMkT3rGXGM0|94A$wamHsrT!g-7Y_$ZILcP-oNawq-lPYf@@|8Esh+j7 zrH6+j>Y1EttoRSRr+zbrX5CH!@J^%UJO6pea`laoXx&>sK7P|G{!i$m zFuDQhH`K?C3Owron)6blw8!`FMfbGz_53<=TRJ=1gO}tgS%asD*t&zyQ2ox1)|Srl zD<8Nx+SoXw-tW5+kpH^H`VUdS5A2~vDI&@$(E>-eC$9zI=woXcY&1cG_KpAOOO`i` ze;d^%*2x#Vme>Ob1-w}BUnS!8C<`sm%_rlH2;_A#wz2iJwf2Pl>!iH;%$!YS$pV_1 zdqiy2tHv01%K+G3n15;{Qu!zyhIuHh_+zf1OqQ_>zqnI;x=6?_0dO8B&=I z_3=BnJ(!m#+{+UPSdNhoDkY)#v} zN5J?N@orMw1cf7dN9?oeKYtvPC?0u#;ZxtP_WIdbnSkD)c9~D30|DcbKAGiReQfEI z@ldtUH9rV@=*7@a_`;lwn50#i7_%!M(hK40VG`XPX*W;Az&!zed z?NA|>aJaL2H*yepc?30{SkeeAiIB`CRx4C<%fsM@_0$|jzG|XI`8I~QEhP$)!gF-# zg{21+&GU|Mca81fa58kgJZe<>aq@!eCl0bl(HfWee6uh#nbE5^$%LuPiL~sdoRi)+ zWd2}*g5#-y1y)r4mZUM6nt1D-xDQn|%GZZ^8%w9G*bIG-;#$4&p&i(qQoMJGJ&t=w-oOw<|Q}am2HU*DsSd6IQ z#l-7HCJz2c$5S||SU5ebp2k^^;$epEml56LqXC4*>AJ!TxviMVmpB{^thPCD#toc- z3=hy$IV0;7#E^`>RPy`iCj%-8BS;?7mk2aX{2IMxp&O59u9+`HR%kit64vF5WY|A8hj3>k;%F5uR{& zjjBFh`b`ZXiiZz_ti$6J{fIETj_Z zbXW3;TBum%_NQ*Ntt$9f-DOt?sS0}ZTuTWW6|twun&UJMl2^@71z7BUM?x{pypIINatulaAZk0~vb3vZua;IHo7+dfqifxShVIbdc>S6NC+p<#(a zoJx39?1LwI>BZqSA&;Kmo%7t{Sd~?C8yMILHHcK~vO;f#N?i(V+Bw?8Qbi>;!RA?; zZ^v~$94;@Lb2Pq-Dsjs|3Pvhvuke|?V6*6pu6-01EJw<)MN{njp{~3;(=mo9thK`JdbcH8+}BSYiYZy z*Jjmi;&4Hn3k5NCIgmv>P`1;Y?$*?J7n66T^bK~l%kpzwoqXEDL#CzqpsC@CMXy`I zOUv_?>T*uh{9MRMWjYP<9=#l- z=mR14=W0GiuY}Xrde!()HgdQ<7HRH&a`#*orPJKqa9KBL?C2~H39h0dzmoW&x2KKY zi5*sAYjn#y;O2Vm3ego0T4&hJs6@GAQqQjv8I$GpLR};{KZ{|egbcs8Q+O^5IXDp_ zVtdq8&e1F7&?&pfDq7|OsjTQm!3$AEL{}kg9Pv(A*SzLttNDF&fEXd5Qfx#QPvlrRA|t zX2c%u$FhBnNq@nUlOR#KaO`kPJB8;6jeT2b^49#)x6uqn{?4Vmj6bjox=TXf{$Vu@tLtc0)$Fa8HEB;`r{xRIRkYQpRuOU^qH>vhp2$ z-}R%SHg7J-;d)oFT6>Y^*5a}5GIMNCapJtYkH7nMon3`t{98ptw$WKiD=DN{Dp9uE zoc2vtlg|6w?j9|@<-~tsF%!plM^&Z;7TMfAYcJ~I1gKQO-3JByr5!d)qc5uw4lN|h z2*F?4el|k=go(oaZlBoF6cHn)u;Y*<4X3jV=0UDQ_4m5r(E~*o%IzBN?XdGVXaZO} z%C$>0Bx@tZiZ4D--wETzqOj$};@Q>dP2ZgM?wBn$e*y((_T&31`=TXNvSM6)gVCs|dro-{opJ@d+Hw|v2Y zSnCsEA2C_>GZkZ1`ItmnM-p)b9gV^(@QZaZnJgS^^V|aSe|*aclSn)zd6!I|-duKP zH*z{+Xujk!PLjDvTD)V8?3M;>6FgFUR*y@@@R*B<x3oa3pMGvtmw*!nVeAnoY6rB3?g#9iU(WUAhFo^ zU6fk}Xmqz3d1KOP;@*6rEOj;TGP$WHnLRfBL*71|@-VU>+aS9uxDKHCnFCi2)f4s$(hQg=IL%M!q)I6NB z?_C#ONmjY-nMh?i7mobbvX|x&VZId-1jm5!GGx z!t!wHBv_pRHfLQ<{XU_f7`*v6rCBwuD%kA9X-at4-1Y~&BS;iICs=QdJX;sC*`|HI;H?{~XW0*R!7!ac=O{x-#u*W$A{pm?XMxD=o)P z@jUHxyRtHKW>$wJwqS6(=QCksq@zb#fm~&y)4_P|>qKYNDk4I5xHAkLYfQ?_Pw0Nz$j}H>= z6yMM|G(2%`9t9x7Fc=VaCjnpx<-~-6IO64&{lz;1q@<7)8c#NPVpt2~%Ypzyw!qQD z6s4MzhcFXgg+Ae5f$=3fFgcJSwe-$}s~7}MY{9EWF9J}!HB*x(8tyM4*GULC+dQCeSy59tdtC^9b{K*Mk&+mC7vw6HAF!<6enE$!(D36J?a->#q{qSP8Q$OVEF?xyO|o9fGHAYu{vL;)*W{0 z0e)Gc0yh35PB_d!h zy2L=$aih5+&}S|wAE{a>o-agR+SBB?4P*eMAYtii^fqFVoe>B-;+0hy=jvL6 zU*5A3o8VO~_?1PjJ<3eX2A10G`G7oXo#!$zWYN~74>Thnnz?O_rQh3baZDa{r^Mz4 z-pAG~0HOh${A{+n#CEL{0YrlWIQhhWHSgusLP$bQI8bx-Jp~j-cf0M^N=l3!1_g6T zLMVPULpca8E4a_bi)mo-5WG;6-7B{XMLBwTZ~3RYR(0eavBhp?BsaMKdgYWMR76?* zk;4abyonL1{`C}@k?P=VY4t?ggr!q`<1jQ)HWo(DLac{3SM4#J>+-DPq-;pAdbUix zLB5eT1-BVlZtBZ^*Rf}nx_to?^$R|L;bsqWsJdsK{u@WbM#i34%&qEK8V$81II zt8Mtpw}YvH@V;i+`XJv0j|ywPvvA+uD|XuH{MJYIAkG|}=&d#0Dda$}e?e=HP9rs| zZWhvZyY`nKN>1Y5XLSAgbt|d%=KIXVcwqqfaQNy=QFiRiyi<}|(| z-g$Fs@aA21xslGoLgp4`Bm;!&2wLM1)KG{D=D};h^{-{g%&wfFb9|Uf!hQ{QZJ}cP zn2<}iK?;{Cl&rT>C1tIE;(9+ERH%qrFmj4%{g%ck0qOrxMG&ufiG9CPeDgahV01kcK!v zj^DaPEs<3_ckg+P775va_s-U_N}nc)!=j>2vS^-iZ_k#c8}{=(%5GGU=I3B?#V$5i zBN@+7MKj;1@XjqcPl)JBWTC&0Te84R`~r1Hvi|QbR+E+JWDukh&8au-+R_TIbnVTB z^*8J-KV%qogZgD~RFh@WGu!d)DcJo`_4csoqJO*l?^s(HIgb;#ShqNSX* zJ`)sGZAeQ@BI1&Vv#%eiwr5Fm9t@mPK0Qp%VQ|Z&d#WT1JKW|rPfZ!N) z1j6k_)Qk4aeMJXq?a1t^0uc&v>hWTBLy8jb8GL%ZC>d`jbEY2KQ{)wR>jZ)Cy zi1r0Haa}m=H1s0=eeaN*y++G?X?P^Y5&dmW_J=MaW{dM%77Vst!Z&KHpRkS1Hg?S2 zYtE8~w*}<}l(yqgVbSnVJCwh*Q8LLLsO>GY)hs0&No3Py%pjRN(xgir+{4-!8^4`R zM``$om$Z~r5Buwj!yjixAe0m~q5Yy(ntm+y&X{p} zewoJkYSch-a>N!)nK()J$0!ADZk6b%G7`OdUk+?2@W5H6W0xeJOK7PIeewhb)y7#| zH?8n_aiRA8NY_MimUIANz{_;|7OD47tS5%=WrrkWRkA5Zlcm1%vB}7iRX-nr*V=Iu zWL@-RxX>5~VHbf`6^)c{NjmaLtq*K_lTWSZ>xV6>JKIRx(*;%e)VK4nr&Z1Mc-&)o zQ)3lA49wSdZgL>pcJWO|zCBl$zrOHLqYIWR8(c69Yu7mYOF1q&IE&w_Gf_6c=EBJj zAWw$hPA<7{(_O=}tncd3{WFTwL#YRMY6iZ;>=pB!%qcif+c}prQj}hBEXZWEbV9W; zkvI@O-XVe@?K8J#e^Kmk+HhG!qZ&CUMMg7$-^sd_E;y{Ru*+|@u(I#a`cO4QDj5T3bgm@klz z$@VOA8|A~-;sczRj88pva*Vui?jgRxpoeWXk$BYb3M+ll2DzS#-5{!=@N!Pk0 zs;#dw)S5g5Tv@W$NT+(O_gGBGd$5A%dbPDKP|45`Mon$+%BU7{?J>FE0Q`+wV2TIX z%=6>C8cIrsrDh)MxAfCErdaHCOy)4O>8WyK#kIBSH+yh{`QvBnERi#(@CuN#4lx6f zL_!w=q7^KFM=!w&^X!h!di^L*Hc1rLrwjbe6W2!N zm0d>>&w-l5yryN$l^icjLp}_p#`sUTn$-KxBz&ay(NfhAb)xRRl)J|g6)CJBnCC(V zcfR7wGJUE9GdpX9Q|&u-7Lm8}CjAaUyCl;qcDOyN!Jo!N1?3->@hS30wzW19$;K22 z_>A{Bh>|iZsi>S1a#nQhsbZiDdY6FcRVt)-z{4YU75 z+D}D3A>y|kF~8zi3rM|y)t)ixy_bs(-J^8 z|9~S1gHQAIWq~Zy!tvW6pgvVo)k?j@Z<|_?7a1K7$p;{55(}V_hMYSR&lxc|iF);) zGd5d;z@NpcC+O5!5CtMfho-7vlii)P1mS}XfmuaN4~8tg3j68<^7NGF<9zyqD%GBr z+4eso=~FbL@6i{sLdnb%fD`Mg@F*fW5#E=_28a{uR71yi_gS}{1UPMxffy*dh{wx2 zZ*Rs@r#gL@9@=r7g8Sn=k^mLoOV7Y1eQwtIy>-uLlwm$l<9W?dGkfonCRl*0Zkc%<23zJE8mRs-yly$BW35`NqXOmygsNvNpF)e4^wsIYFX% z)4!CbnBqX5&MpuNbmK^vRFduob9J@$8LH=B2SAn?@hC_=6t9Zc@k&uMX-=&dgk$VOG`_*rXz(=rnRaXJro70!;aG^ z9f-LreZKhp118?t2kulLR)t3)LgzocCF%wDb9txmA*gQg;%oKptB$rnkkypjW=Xq& zNUeo;IkdQ%X-Nk71Q+N)XS{$#PAdDttxv(9$)|C2y zSv%O163Yc3$)ZOB?icw$41IDG;CAW*_#!t3p|7%@yX69Va@M4mVq~=He0)hCmc(h? zUJ?7wwn=8;-J?<9h5Y%Xq~;|HD|7K8P}wg7DtW*$e@8qj(5tU7dG<;hgQ~@8vq1f; z0Qec+nvselyf)QB?FistfX`)vOAVdQhK5TkAqSL$fCrL6&)5(lH%r!EYNySNRv zsC(NkamDU#>sUVCmP=W~SpkG?;XHGg9NP&A zDXGBGm>q&;Y!ysr;N}4L*#KK3G(1dsD1`b_74^ecxY`D+mq&FH7;nHL@yJiz2OzwN zNj0tjc-ZHplHQ}YJm%-7oGE$D@oT+CQnt8*uP3qToW4~8{7fM54}R|=h-4fd7VfL0 zHOx24OaVB(7ogf>TB$s}?%Y@MHQdOzlH%Y3yH$KHa3#@=7}y96-1jJSR7Vcf`3QCj z!3~Pz{JOkhg(1(3L1OU%gdhWK-M9qAR$PZz#kW%Dr!TdB_%vP%Q|oUC;)M0q2v5w6 zbUhk>dSw_DYlhzd%PjJrd1K3IUgx`$>(fOdrL!q8CSYS{qbta`O!QF57^FNzA)m%7 z1q;mUB^H2lZSooGP7Z#Ut6pw))!~8h+rmd9eqy$*NifGCa-hTX4!HB@La|*wKe?kL zFp1^@_(|bW7{N)Cob4U@J!e|#n31v&Wq<@bP04Et7M&C zE@)_Bro7@mG$dnO6Ra%xVG#zf(uq5!rI(?UO7+0N-c@0MLaGg5sULl$q2Hn~=SQZw9Zvuj9aH zCME;E0sDrn4O^WO#`x~0tD^yilM2VZce1@3k`Wi?1!yvK7AGktI{LDp1tJu`FQ3s@ z)98i~DuiexkKO2QO95cVECJ|BylU*unO8$?sRI;y=|ObyzZG&uBblQNXdey04{B$K zFbfSxK5KKo6f9S!rJlN9MukeHK??+yBqxC!BqcyY&_%LoM&gZh7wOh5eO6DHp z%|{u%Z&)ZtOz+HuMl{*(Iz4{}G7zOScZfKfuo~9&Qv;xO7eHvcu?Oc=DQahA09Kf8 zmz>B2xr{@ZO6<<^hfX0%&z>lPHHV<6snB}QtV3dLZixtryZ!Q5n(!MhvmirK)h~Ax zJTzJIm{*|FeKlVBreN(|^0@pfGWvK6ow|z7M*DQ@WJ>s5D`W1&Fg{n==dW4e{Pg5y zkbngV*xjkpKOU%t8YB)iNUe=?i8fC2aF)C2vmFUlY2_##BQ5hD>*fLm%e9k&;(hY_^VuM{LCJtK!1StqVv<36^tSYkos_C| zO3w(6104Z@U7h8Ggx4D#5LeI4NM54fGCvP4aT|p5CR)E2390Tc?^N%pRqTgHB2Mr@ z7TcAvdisq+XRx_ZgkCPczUv}5BEpD|-<~2ZG2&db+RP|M34wD3^ z)1VlDg=12gc~6goQtq67_ei%b^T3F`A&qo;QA_J`Z&OLGdA9lo1

{rKTADcCnj%!n>cx$OHp~tfu%r4s*5}Ztey6Kho&WIzgE_3UWo6bVGz!)3)r_y znLi69&qsx_Pb8pf<9f}!`--x&;Uq~FY2X2?cr}C(^i73R7f)rOS#!&iI5~h8Y&$L+ zUNXcDQKjxu0XpHV3Zlmt4=%M5iYx77B~Ba4Q%g8&4wA4fSEiiHb#bL85nIXvKTPv2 z1RUfnDh80G0?@8KMyV08%ly~TNV#$#AM=(r6D#p{8`7#+Iba4uKrougQhw9%Gl&Qd zd~bS$2o-SA1Y3hqZAM~#)44IU(q^6x2QLEzg@s3U;^o(9o*^=3v_MF!l_@dfSnhvNCxz>&wsb?;o^vYcLUy3kw*8{% zl?#Bf*h4U2=`oXw0~EUi1wAXO@ST;8l9VX>6d$N%<-ImvhRUz~-;eGFd3R)g?)=A* zVi0z0Ka5rO-;sIfM)SdccAQ)69>p#TCP=aziObw^j<*KYGG{g37ti*Gd_Mht?AbM8 zhx<7^z`ToU!Qx;8V0da65w>Lq6BCpEh!2mcfkBeRC2{HaFlZ0sB*-+?0s4ISDx$}x zQpd)QbdnKF%lvTLS#?A^l-Bu?Vdr#(5!5;Vp-R!vYbx?jX+T200 z=~ya$1rDPEhhf)Jm-oziW*Nd-Y6w~Ap+DK*;zlov8OQhcr59gOJ=Qc$kWOZs`W121!L?odOpL0`6KW+)aSO?I5m(dDpV zVFt|R3D7}?Y*b_>7JxQV&JQQ(hFO?p-yRkUOWj3yb5FUL_u^y| zQ4Jsy=e?LCWqKGE$|qsNZbZNx^uGp3>)KdVs|9SrHJpu1glXPLHl+!XnX8Z5eatr!AXCfZGnN6^5o8-s)ibT@2 zZi6i^00s%l0og4}ME#@DgGgd3AqTyb0@EupIoz%nfGq@rT0;-)l_@{&lPE;oF3DWi z-Wrt;ebpZJo=paRCmEHa)qrY1pPko8J_*t?CA#6QxbuoR7h5>TtB11|+fumOa$0>X zgY?>p9PybgBq4(>v28gK}95VUdiHl^D+0uc|X7Tshj6D z$nWTrKCSb+QtE$BeYP*4I9EHvh>%u_{o`OHGajx9RAQt>SK?~O#NxuI3BS?On~a{> zz}_`;f)_rLuxKQyT_R}qF=pCIIw=iwvBAE5=Ym?O;Y}@z-Wsoyz@w(ihBUk)O1%B< zbzb(VDiB_#+jy^~+2VE_Vg{g$|MfgQ=7Nk?TZSm?8c-7PKHtGu2(tntOu^#u#XWwE zjEo<#GybexuV?xN)XrWV(u@FzGzF;1w<%i5Kof(D2jRmq#!GB7=mr&my@^O zPP)%}Vjta;hGOGl^98VCFPdLe+;2(9=@U$h#j&`gDJ+tEpS2v7wP$Zvd--;!DFc84 zV>pQV7jsjC55}TnVuX0u)h;51Oy2^QH)(Pb&aAJHDz~FNFyt9vEFp9u^KKXJ-#}zx zON7Y;eYB|^jFYUv@|S(Gh+szU!;<3LQQE;w{0=(a@X1x&do;~-J5A<|Xf`0klOVst z?L5@-{>AM?+u8A6NC^DJnZwLsH}~eMm3m)4j4OUHWMM{9;UtFSE+J@^?{i#0oZ7mM z>4^hBG`SlrvdoRUt!juYD2ip;X;kCs*m%FdO!RJW#8v{WIzRz8j+`#lul06O!en!X zpTsrL1rCl9@CkdG1@B2EzPN4z(u^=U5S)t~CCyM4tBbPO-L6I|koU}!4>ftyJ9J$y zbp!yWCVZ~p(|96?I!}eDj9}$t>7w>#t<~a)cs+*667$bP*@4sQ(=D;oQ5xA#?-A4A zZwUG%+0@ju&%ZjKD+{`)7>YL+a)K`mst_*+$eIF^QiYP~+koe?{rK*6%PtlGBe$vm z&;#8rJjHrP$njyCQ1D^=u<*+{T%8k2cuq>pHva8#Tct6ncDlc~MRI;ukO2rbfhwrx z%7$BH2^Mz9Mam@mCs}ePCTT*>PxQ?FhR7dSnl?6goGZnPek)U{7%qnwg!~BdWQHBt z{%;Sc zB{q^IYG47A?Czc#I&%DEX7`ZmzO-!Ub4I;9olMU|@Qc*Ow4f5KyL~bmGX7en3?$W< z6QsUl(8NwllM8H6nK1*QVsQ{^9oqUsD9DN0!icb!dRG3YI;ax=s+&?eO z_Ivjfw)1%)NNy$Vesmbz>Onwo*&xa@30T9Li7)D%30J%qpody&P+qFi<-0;kilYSq z^i?uQ(eTKo5u=##9=on&dl zSa^GS=9x9OwLCNO0L1roTGN2qrE+)&T_mw{;|CTiY7yrXz=PjD;M2>}k#C#|{Hm@_ zw#?XD9(~MNad#s%$a^N@N8JmD2u=bjO#KKEovV@Jlv4h^Y+2s^pxI2o1Nxs8OBpbQ z%mBSlC;})yi#z#zf?OyuP~;|4mqR?{N9gI1J&}M&Xa%16JP0rkZlk;l$ZgeuVk%7p z*$owggtHBtarUjRM+9D#=<70PQXF~+wWwx%qVX)>& z%}+M9NMK;?SacKn6iI1L+>=wZzdHOt z$yXlIGZTj%c=+kDmy355o+1v3n9ulW1pN!RSaa&E90!u&9gDTGI_Q@HNGRFZ+)uf} zwUGw@XlO>oza}76t}jRGvz22ad$MrpM6Ag0Ut9voXAToNO3SAgi%Zy8S3?F42Kow zkP$?8GO}hC*$&itCxe2cx{Xb)l9Ezd!aQU|~W=xZ6ps~9SFu!CiG_UU-ZJL3$6$8=e>zo#|+^>HJ zlQ$UI_VUJaQ_!+5M^79&BMtzk-HpUulHO^ogDsEqbm?};2G_l49a_AO0jNd(q2|gX zVx2b1Djdih{y|Mv2NF=qUz?ohE;e_!% zqzl^L(Z+Qg$FWOhx@Klfa)aa2-T_hFm>bfaT2e@D$azrS3~9?CF9GHTh4>d=zxzsy z{%S|3%T>!k3N91kUJfoXKD#?q;-DH?u)lgQ;9S{3jE|NX&nEyFit(;Liq!;VF*Y{c ztRw;3R;twPCRkiM#Nat%M9)~RQkyJ5{rF##x;cka)5lpJV{azwCp~)fDEom@s~a&5 zg~{a!p$j;tCA>JwE8YP0J3)egY*CCN6m|*&(4wnlSQ3Ev;4v0*__gQuL-HE*$QrEK zE$jmTgo#jKvfoHSs8Wyb9{0Yie4OGXnbw=;vLn7vRu z8WtlZ{GH?Zf*7c?8WPk)O|P0p8B&GEU{FvY6t;K^yaQ=;8Sjo-MaVIRP!Y|EgQwcNA{ymcckE`6QQJkKIcn_HRG4oHa z0vozPzX#{Tf+(o776zw9GpldJ`KAGQ?F>lW!$prw+VA$OnH1T-BqvmV-^ z;VFB?{vHn~t!>wu)B>R}q_r_apj7Dbxp%a@LKT=gL)V^H3NCQ0u5N?KPBQ{z=IfL!WY5AbPEgvuNrvA*uePks8n1DhOv zFu_im88n>%&`o0B*nAtTPPR#+ED5heXZKCO#c~0ch3XGr$^$tg52tF4k& z*nxN+L@;$&=h;cJ-FuN^Az_`a6{%1u4P*wa0rwF^G@lQOiD^<=7 zxM}AnXL5P_qhJ#{J&Hc>W2Uc&otX%3lIpYGn_|b5S@0!e+qTUB0_?q7^I5|lme4I3 zt|TZIjr7?XE>tB(0fMzzh4XH}Pbi*DkQ}dS;<~c!xL6WA@zpsZfSVzA52?`;<6iVK z08y0zXuEg7NxV*?sL!(i;eN<2Vj^&5Yww)=Y-1J9i?hGl@2wr)L4#%qHAE4V?;|A% z$y9#DL=v7NF)^a1&%~_j4W2m;_BkTfg?)}jF+yBQdVmz>yv^n&w;M?%;_$4Z&?3lk zF3~E}N~>=gcekuj8NlX#6ZNxJ&hj9w$KNGkz_*96=>veF3CAf1S}CmG=jhsCC#qmv zX&y)<2oWS5HRdHI~&O82Es=`wg$nF~iO7q_n7FNIU@1IEI{t_f)=qUR)BF z>E4o_F;Kv=0Yr^Ti3BADeQ1Ua;9H+_cAmSrMFlg;Dl{(#sMy`hlD2!DmMQ78(}HRm z)PG}b?w&HtQL+e9Z7YjlqmOE00Mtezz^r%$H%y2~DR*n8aP0+oRq2r?{ugoE-hmFC zw5~)es)fmcrVE6DT>Go;*YOhvD~WBDHO(EoEcp_m1rbj}M$FEF zO5uweKH}x6?;o~&$z%7{3X;(@K+IKWt@Cl1$+{(`1rjG>$%y%J5#XTD9!UvwIQX3s zx|it*sg!B@cLjD?ly+9AVvouK_5p>ffOi2m08`u zE46F8P7(q#@V&sQO&bEM_na8$4%4uxc6xj#ay@SKMdW+oHH-{xRQiN-wi;Y^=Io+)rYJ2U+p! zEwZ0eLW0UWsX2NL7Y%T6lxdjQe0`f{$v-o7yEablQZbo4Kqu--bFzojeU&;nOgGrFTov~Zu^b=C$&70}NH!McwX-xIp}TX0N^!?{XJN*>v1i7UCy7kz z&YeAbHdSI|rF{r)3E#u2l|~Qv2ca9{#Xk5f6x%A8=mH~hj-I1Qin;^Xa3RGvlh+#H z1L27HTLW{?RNo(C%i?QY0KWBDf!!tQuNsC*=I>G-3?xA<52wM!6X~VLqX?vkFP3Xq zeT=Xoij>jKk_s=gZfz3ECZ;-9zJJd#y~YyOEDV?8ql2~asT5M5Mz2_z7EE2+IwJZ4 zIJ^hrRg@M3N6yAhaI2i+i!S#bGy4E|aOmeeu_IFB$vXpFY1L`MfR}Xb7@3~vK5>lm zh<$<_f+W-_f9IIHQ z2XrZsvUfj;G8sZ*F{Ir}2l1v+KhWCv+(>T4$% zkwu?W{}iN&#jqdj0<(_%JnQu=1(Row?@=!o8kM7ylgC(2?T^1v86RcA{uW+Z7Kj+| z!4@q5ZEZ%Avcn^kbj@8aiKQD}gXLEzKO1CUshAcTr$TovctIk?u{&D{U#j;4R44PJ zk0rGfym5v~r5kV`+oZ^mwj~Y^7zfWIM|efIwfsF-NT;j%_`lMh?L508oS|#lMDDYHJz&n zpX~>8Q3%jyhvsWt&#){>5Dqhy#8=bwFnLk=X%`CZFpA zaH=hlO;a%Z2#8UgKOhxjp3#ke;>mLtMc`Pxql_j}@tr#1S{;3s8Xm>DucBbbhZ{Z+ z$G(#xJ;t(=MjyDkLZB9N%@$shkIOf!5s`X@|wxOe8x`KeuR|1(~0k z%=F6RdO1TI2G9}LzA88aEm^&>Iz*Fae^m+6oUpM~htC(3a=Y34tFo=#lWphJEE4Xd zC_#E_eMFB_Gnb~ldNfswbpv}V^M2TT%K-gLP0I#y+ICB=>)}{xvqxUdE?ijjUOaDG ztY?WM0J$Flh;PohxHC>BHW=4rpB%9;=z5Ap>RQ)8n?OK?wLgR&D*Iw`Kn7qUxu=gU z^#?~{*&Hz_|Fi}5m|Cuvl9ahAgKF# z&JA%Fa?^S{=z}ElE@*%p7w*P(@&!FtNl`4GfRBahdQ6I{o?_|bAWh@KmfeRl|EO>< zCP)V(?(^>Q&QQI&@=R8_2Q5BjpgL+7d{|f`q9bH7#72N+V9PnZ67NCnG!rj&T;(YP zUgD!MF{h4fh3M0|A|~jKE?&RSPumUezc~wfQ$mbmJ!(m_kQA6P zHq~AVQt4Axyf$G8+oJDL!|K8g2u@K`r};p(AVB_6IzqmOsn>1ShsK3&&Y>%MpGiZ# zs6Ad5+z^KMBN!=wdY<$cZodsGFL&pqVp=Q)FsT&Jy!}wt0UEcq;IT4V*>2ausRa#l zegpV{m)-Eg&u}3kKjDz+n~Y}cdPUl~eRS})T8L+fBlMY?pyWr8k`pu@SF{Lf zBnlf+I2#3(DJUr#WK`G_hDH0@_oV8slKeofuqGQzK0T6Q7t<7OR+!)rmx<-Fy*7k z@Ip3|Y4!YeqDW%s3I%RGXw2uE-z(vrZuP9D)@i6FDMxbY9C+qaC0nxF-`Aqw>qXU= ziro%)h--Vu+7WyFG}#7gz43*_?zVe;7X|DVLmlfu6g)b?cq+KG@Mk28W{;T-qiVA+ z$pR<|yBYh^Lg*n&sKzi@gHzIu^lVru)c6F3;-Y$y-txc^jKbNrFYdgua8&3p%kQ4R zL~VGorGabEuUq0!Ub;T+XplijM0h7nKv%6bMW?T4{1u-^kdEqfAeG}C0?QpPBse}kN z=4FQ-FI$gQ71O`@!k{_pF~90q8~Xj+u{|?4N*$q@U+6n@l-yRNS?8NS5}%St6S-gS zi4s1V7^NyLZK;%gRnK-jT!XfaRdUm{aG}I9cYhgec%9u;F5bW# z>yk9-X{MT#%A*h{OFJT7I6b3;JwBtxCz;ZIFn)9xc^S(gcxcNfCJzrj2gUE%wAS34DAd49UC@w;wM|WdJ@Dm*Aj63h)M~pnY zi#$J2Z0&;7)pE#T>Au`@tvOWz!UJj?&_gfBfD2EHR}=-2*8SHGhHJ3N#>R6F6h(5d z_GY=(40}GG6oVm2`Mp3E+%bnn{;U}QkWUU{wI)8iuR`x@P2TsnftGO%c~ z|H(W;vX;3$>NAcq`Pi(K3AQc@lflqo-kD=BRC$hgnVEbZQym$!7Tceg@lAkoTq;u8 zyzzuo3he?P%YoWBUJ=P-D*ChRIm3%%+-8R?M+;y$l1j2%Q@iJ;2+#l{h3c2;m|#kIu58K zxMA=om%P*k(_9;S&%u$f4 zg@^inKht1bwX2jSHowertkQu*_mBN!BF3FE+OPCa~ny`TD$ z^xEL}R5OallD168r9J_uRBK{K=ft2+vmE}R(w$O3nG)8z6D9R1LI8X zCEGSGLHH>BgsvbqRDdYt9h4&+1>7lJt#pS3#k};Cy|+T#xrZW(Q6Fqb$d0X^zbetv zg6!A%o&}+hdFil;iI>WToFnj*9_y~-H<_kPK>sFrOW$&7rpJuW2ub@~n)6WK5N5!n z;(lsF)dLjm*Duu9vaA03B#GX*cyM&f;26sDw|vARVvwt67uV`i$Z&ge?kaXgaf~4i z#t%&`Wg9IQ^&FA*KR}a9hf(>ArvIpk^WJIbTos6Yo;D6Ro{_5x^6AoZwh_Z?3w3U2 zL*}cd*o0v`SfiG+&wxj`Rqb75#@WINvs76=N9p*_s72VDoEE@S^n9q!r{uGEe2IpV zAV2L4z1cXQXqVPoiLc+inUxIqKN|ih<4VN|k%+(HrM1CDur-YT3FS{Sh`PIxK(==A zKvX`9ma6laLDM?c>^OpU6fG0<=MGkuC+YMAcZB?;a7AIwx?lE^=Qq|t=^m6B2!Q@U ztFNDfrKjj=^=nGaC^`&RY^>WjVaeM|lw05@j%Qf4vMVk~ZmrIE^pxOsNVY~Y+DnbZ zelsYIzJJIjoFh83MvL`x{>EhnQm6X*zsxU8<6tQ1jXxF6HsIVU zjk4*DxKm@zdnF2;uQb)-ERYsck8T<-7;H+WoKF#~3U&!Ib2Yi)AxVX5V=9(2TOtiQ zA0qXHIVIS&L&&p|YY>%meo}FqrvnC4>GtsS?QRBa#10%0{Kz}u;3Q~7-^dlD`++RA zk$KG5op}7m8Q0N0AJF^H)n5OQ81kd(5*v%L=tEx)8Kw@K_lGa^^JppCt&!cG_8Mj4 z#(HuUBU;Yi-c0)|39XIJ13OTqnO_)XaahN@p|rm@ z%@GN!OIp{SX*Vo&l1breyGJ%a5igyT{Mr*fj?HW58wG^D4{@4R8x1e2%J@Hlfn((h zc~nV-wX6|a*mE^Kf$|;Oxv$xg1lvIAnXGCaZ)^H%#6CjJ1c*>P9qziMDfYAczVk#t zbe6{~UtqjZmj;6de*XS-udaJyEde`l0=e zD4TVYbd5nxLlZ->zS-A2$Xy{WFzzylrBv=gq=Q8?&#dvs#7@8owSt(|0gn@9a~TU> zktxrjj4v5Zf)Rk~;*0cgs+7XP@Jxz8I4P&3RC#n{IJZYb>zw90uW6*jjh-|+ z8cRv>=6eWkomE`y@4hS?Otvip59}HHMCDaVe)vGO( zNn&zE=b2m)+Wd;GH;e$m3x5hOe^V??7t>=Z(W&!^Y={RcCOF0@~OcZhYo8hrn1xAPm#Pa>=rGE`wc+hEsvJtSE?j+7l*O zG7wnH-p}HEelN>02=fSR$F*(;CMt;_eqbw)4X4#lyZS*F^&sCiS+z;?7QFn+%4P}3 zBMyTU7u=a`3d-bX+Q7MYS1pg&#$MVUpdM&GeIjl@h3<-8PtRe7I)~G zBkt4J6c1%;#(mq-Pj(k0KJGqNezy+I6om60VzzVnpK#P1u8*A3_0qn7<8`4nTWgAJefTt zAHZcznk2MFre{_@LAzipSDZZ z5i>S^Q_ydepKWNHo2A3ZT;rjX>*J^XFn)OgBD|2P0MhpxM2H~4cps?rrfB|ufS;B1f zdyaO}Ck;QH&JWm>8z7X+xcxZu0kRo5E}<_!K5w<_XM&n@BHeH139#rK|C~7-srqO) zO1`E_O}-!VqgO&~Kv^o@-C>hT|K;|)w{$YO5#~rdZ%tDy{*BSq{8#p)d+A@6E{f^D zsW7;e8@9*oG?D&wNi?}%>@}^Vk3zw4O~%dZ@u62aVt(R}SZV9hszW13;O?U2=Kq$7 z--s~$YqVfPBEyQQ*ks}^8(tf#bmzLVTk>nmFZr5Shf%QUP+qyNj&-eaWfU1ykFM28G&vH~^fBMoUtxw;gY zeDN=VXR+KvH(v`d5ZJW-Mvssmf=HfheG{&?a8}hzIL55 zu~~Nl%8Sx45j=V)e;_3qJ+D@i6~q{NiUhtMSPk3TT3YCbY>3A<_J9i~buZyl@K^Y} zKRukdrDt)TV z$5Jkf4`^;GjYcl89;U7l#Rhz-^opPOR|`me8ewb(;0P8YU<1c9? z1)c%6ZMm8Nyh>z1C+X#u*Gv~H+~=$A@>DFJ^qp%b+3{A62;k-JFWRHzo||?4bBB54 zC{xNINS}?u=Gz%~_DsS8#^2VC8q+X>Yd>0C*~jw{y0LG6=as&ixWFlN=2X6+MO!(Mos5a&XowzZm)JmILBa4R zlgydgcpkwWebwyYCF69{`NDNo=6#HvFU!JI)0+|7KJ;<+9BC(Fnm#sFZ;xB92bdP- zsi4t}mtj0@PuM?$mcvP@AaUClgJbs^pjF@&9dPDWvO-%s>~{v2?0r)G8E}V(R$z6z z{T}K^k#-o*s!J{Q&s&!m?}OgI&&=i<|bCce0+ZPbdayF0+Y6AnY1xkwWL|J zw1pGg?qNzcr!||ZMmh^4Z+3_G>=Lu5+%&@tE}X^i-0O8$R&qGwTK&qh*ufXmu0(By zvm1>lsg(5!C&zL;W$h}ts6t)ilWC(=)AA$3)FpCl;(mJ-rEj*R@R37r5xjz_$TAAWptTI6KP z{)Se5uoyCld!jE+n-Kd?J#!0fw0X_JtVkc#Utp>alXY+1SlYCViN0Er*D_{!X^W_Y zvpb0`#-!VEM!q1OgY3^ z(UqiTq;z$|%=Jt{9%J=Rv3576Np+JZmK@R&;(o`Co*ewVdoRns<|F4e<`UVPX^h~& zSGRHQF$L~ZFWM4-`k|MuwEGDnKMqWJRq+KhkALBlXlyZhUHfgzvF(c6;P7LfWe)t} zB%wl>^EZj9rNulrV~hH)6+wE}S@Q{`C!Wt46ryA8eV);7C&OFj2st-+yKM2edxWoD z^n&y!nrFBZjMC%s$h7uwGutwhlFN00+%s=rI;F|QlrNE9t6Y}LHN(Guu8L~0WlT8bj-J2kX$PDnsNynsp%(Vb}jv;3n)$4reT5vBKJyIIeL z>(IZIyN@a!2SfqRZ0gF{^e$b}k!ynHG$-U5N7kBx7Kbfs*_aBW%YEX#hfHCqaxboY7sq+CIsV?TK zSpj*PPX)ROv9YBq`V%ai^!3cQzrMT>h8i?cv26qC^TN~Lz`;Y!(H;xF?#?)?o#sre zW3&L_qm!1^?#v@d=?|pC;89A|cKMR2{<3RIJ6Hs4aTvXRo+3T)A#4UXd?#X9l_Tc* z*qt+Qtv;=N*tjf^mL4Z&Y4)JN{B*ti<*Dyw4q<1-FyhU_l4XT+A{l}FFjS}CXR&*r zQMfam^^4IcCV1(R(7J4e2t{l$F-P z*NHh@e&v@?Il8It>ouN}#dr(<3N*__gq)B8c(0-U*61oNm8y5`epPh+?Q>KNL@Z!P zp?Z6W)q;>zB1UaxT5!#i+uf|_(B%PlVod75Ox7DJDD5&9r# z$9eKwJvl8m)3L#K!;@)+zoc`stjxqP$A-&LeQxTR`cO!D-GN+QGfR=3fEMl&5|VHX zo*i61o2ysv779N=qDLbFMhHF}?eV~kSik%c5Kek@M8Sw7?-G)s9AM(~>G_Eh*IqHz zpX|H7^)*tS3Bx1%;u|4@7L6+?X%H(8b9^Qh7&$cgAYVkdz+dl{=-#kLBQs}>i5Cl8KRVsysJ?8gDqz`}d60^l|gEJnR`5jUyb{9Bq4VCB4 ziE<~#%xNb7GeU@DaAyeGR3{y|lxa?l`u>7wj84vdM4{;+WvQ^3&FiqBonE0=OKJC? z(5Vl>oXN)Z8e|RPgXQDkHvL{jeZ55s_YAqY$CJXR8R0!vc+W8)f@p+$tpQbVrjR&J z-p-yTG1;k23lfy$Ibhi~Nz}Io=T8$SGP#sW{W2ch{e~4cukM1FEdr^V6fkXlfUdqL z7@Tuc@WHunhX9Nqkxsn{yy?<@WtYo@uu9Gc%TiFP)VU5R%0(js)$sXw={iLb(ZVXP zqm2afPI%cj(=rFGni=+MY18A5BQqiJeUf z<)Q##*>;oLPjlGNI=rbJxwF ztsyw=UUcdqr6v&_u1;c=!<67eoxo7q~!a}#JPKqZygX?_Q4Wc`Qe6r@G9#5D0~RHbY9q# z%fYkS#BkfJ;X}EVgWD>>u?{)Rxw4c34G}`REAz?-QrSd`YMl%_LK8VjQ}}2yw%J@5 zh2+OV)-S@PZpGA&f)1hVzxfC@P_@IINm_zq2R!^2Z9)$2_W2hc5(>53J%ax!yhR$= zK_zP7BMRTal5;KJQa^dtmz zCh6kXm8un61lv#>Lmjv$lTeJppgL(z5z;!Hom%FlXzWzalWP(}2jg$+>mVCi7ig}C zLL!L^pRhl}(JD#Hze?-6cY#ZC_bgN(6B?{c_nJQO_%NTeI1~ATqq8a@v;>8VR}d2u zOD$QTuKc4kH2$N~r5mym*+K7N3gHsn2(F#SqE|Htxx3m-HhxGl5%M{r?ZsiNOav<# zci?K%`OD2QMI`X4KVr4S52brHVB^GD7}2| z`y*cz?q@8sDnEmXLLtXayiI)FL>=)!0AKf3YN*#3 zbL|h5;~#&Cen@;5QeZ%p&?*RzMH0a-{_x(4f9F#MZQj^Jp>?D5i-;y|Uho`x!BD8b zX{Zjr$Q)SzJ4;*TX#X~rVGNwreP3bIG$dH*cC4GLv^T#-?lrzqac29=;7}zP9Xesu z8S~l7(Bk&5{u4u0ZqHmtYmKj@D?NGq(znancU^);}H*hu5qdcTg~)55oq9E z%0`^()oYYHXH1tj$VM!^RdgrkUG#?MREKBlr+Ka0%(Ii7@sm+6`Vubk5ogfj1a(b! zVOvLnH6AHmiqoY_{`ham;BekzfIb$KG8nwAG24*pJ|J26zTc##@O{3Y9|2) zKF8JxjM{F)e1}u1vpj{;M$hTqSC=_JD=jiAwmNIE>g^;^Nu}8bH{GbH^t`#IHL>Iy z3cIEO(Sgv(&e`Y0ds;qMZjZKmPTjuJq0;-J&#D>%r&fLM^bs}!ctv8yJ&+vbbWcy5 zeB$dFSC
    IH;2Jrf~Ct_yetJ^JNoqVj*WfWAC^`C;GF$Jf)&2iMS*eq6Q%ms06v zhwpR@r6gELDxuwbE}W|fchh3S0yRt*00#~qM@no}As3-j*zw+zx1V{{l#$|_Y{o*c zYg^6C?NR+Xzp`luMU?OT&;_L4V#v-!q!%7X+*FQDQ(tz!aL#A>Ov!$mOE5v}B@D|> zAWWjboTv-IV$zln;RGIUAzCQ&Zu+8OB%dO>6m#z1^^uH*5KO&p&ew?l4v6L>}wc2L%w{hJ~yMnM0 z%4{hNyKA`i6X7G??C1OS%CCNDNtRGMRkNnilc~EwI`~vD27E=0rDPdxGJBLq@YkAaYBsO0)EcMq|j_F<2KYbqM)lKf?xA+mW|Q% z0y`pSkCO$uQ%2i$O6mS}U z#sj}T<1(++U|{iH8S~`R&v#X`X4_KY=LPZfh+;;mGLOs6aN8FhwSLs0#eS5)~7&14$0|bM0Ki^a{DE79nVjq9qHK3Tpd~#VcsAXD5K}yf?7Cbbaqo zh(H6%H!FK-zJxrLQtSn`=BGU-{!lx8Y>-z?^n1HXB)YeOtor)@dPDr~U*T+7*@N)} zLuDwX$!X8(mykNP!JKY+CTab-?Y^GqE;Kn9<<_8Qgy6<6F|IZR$G1*pX9e-{2&il? zQx@%?V~!x=lk!=%g3Pl~VNu%(SBglC>^)YR?;!nYs5*#yVO+_rdI^`J?t1*#-}~Uj zS4V-YU8^gTr%-}%L`;sRO)+awBx=v}JlIg9prT?F-2^t-nb*3Zs1{hx>Sd0$!lo!K*kZk(4?2Jb8roc)wQQu)mT$;$esVIJ?$Lh#Eolz_vS;q>sFvK;RbcSo^QFu?CG* z8<^sV1Ji?bzAHOr0 z6-L+;M*pu(0ZKWBp2d#Xd}z(rAzZ8rG1!>B4`A=j)z47TK9N34rh+}2B7PO`6RymQ zZKV(Wy$AN*$ltD4a7GkZCc?g_og(oh5CU&re>QJng*cavU}o1d{1jS%TQT}t8_CeF zGv69W;zjgK@euC6*tPg`kln$DlbVq;x7-HFHi=++GC9Ad-)&ZKnWE63V0>EEW#If! zjHnr#RpTlIsvgt}73D3V?L4q4|#tv^XIyK&;C6WhF2tj-j~UNy5mELJeVN!kYaK z7M{AI-zD$2p5k7II(n6{?6{G?uh%3U=?*nlLLm`YF!Xf;LGGmQ?P*BikxG70k2!#- zgAQ1C>&K`~=;)B`;8WL6Re>V7oA|4RTp}`2i-=Kj9O$AhfC5}eYklck%Y(uPv9L{t zm!Ur50>lW7#{AIRyeIeHUH`zKoh-&ov->})SBwIuj6?getJ6AYQrt7Kaocgow>!X$hmRUd-;0B8ANVWn=jg*{n4hqe$yrx5-q^%22=5s%X<6@vdAPgD`jPkwQ;r?l zPzeHNI`n~P*$0@h$Ljmye@CC(;wP33%LsvLZXi{u?0hck@CP7LoJ5%I=0hiUV|#Jv zw2nS!A-obB;OzcunE5B;jH4zk$h;pJsSBKkX+V`y7t)kfHt??Qf>)57z|RcXeJKNe zr!nDD*C82@E_#w74f?)u;KH;e-+i@JOV<3*J!)lfVd6cy(6A+mw5SKa8v&ps4~fB? z*Rq`|v5vZn=g~W`II#dEb*|zCdt23`&^ja;4X1Av0Kr%{v&!?+b*mY}aggN&m9#rG zH!1W0)%sPV{&?ZduP@6Xh^gSUW)Q{b>kcoH1DKs{9DJX!qqJ&>KSU>>?NK@d?YA4K zWGAw$&0Z4TrU<;vg(!1k6jBZgD7Ma#ceeqLELM81c_1K3EgMRxl50ZyiSpuhLrs@( zw#v<&V*onY+k<7)_bx0a4*l!a7lysm{#OLMlm`nV*!KD1AP9*Pu&RX3n~-z?7}wSU z((*bhqBMBWSSU~v#E7E`-R$o)a7+@BPc5vB2Tzp)wm4u+XIZ%YF5t}PVM0Rt8Uc83 zCzc&07^;q&S(|4~PWDU3!Q$u$oBh!VU#RlJWN~n3h(o!W0vXW{@bANOXYIc5a-_>ZTUK&2sa zN5o^%S%*US7eAgAffFfROz}pw>(I{^?0`LPRma0UnO5!90PeFrDkf3PCRjM@dm&)= z5z?khc`Pdu{=dY<8UeaQz*1iZEOk6!sUN9@QQ)dMN@kWIEk{VIqz7%8552pcr?fqC z`=yEAE+8Oc!^Qvs`XPg-{UwsQTThS(p$y~xFK^-5xeA0dnvrXdF7jao=|Lcjc&-{^ zLkI+#m2e37Qn5%pB9LrhloFpRVZOc%chifWbmhHpsB7GUp>CQXXbEn{^KoCIpB40& z+LfZP1|DS0s|H78rAK_aY9X0ZuzRFj0)m4a8Qf&_TSg4gz)+ zv1$r9R63yx8BtQ6ANs~1398H4)3Al*u(mTmIvGMrPKVmyVR#9kV|U0w?69a(@`g0;$g;2g!`UuD_LUbHVTA2 zvZTjT_@;a7ZLGFv1ydO53xGX{b8 zoq*|QIprjb#S0TvCqbDOB_`hfnV|s>;274OXmX#=L+5zt6yDsuW0G6b3M2HCnGZ>` z77z-f3H&pk2dB7c+rgp>`TC5c<1*J3h8shUil9AsT|!!AdNzyS`EE!hcx+h9E~7XR zgpFklwDaG6EE%SDyx2^Bx-Lpz18Xhs2;_)RDHMDP`Y2l3RDeHpzP#`d8JuP^v-9cg zy0kFXjAH-gymdc+`YlMv2|$*-P8R&egKtcV{9S?mINJz@H9>!|vfUX_W<`K}`YNUB_^i>rnZVz|Q!MpUT+dKlyMiIz%k< z+W>gBoq)~UZ=le;uMQmIw+oC5sw__q4H}(-9s5WFB-<;%``a%8-^hM4tZ$vArKJl1 z2UHw{$1oP0CNDgpI`MWk{+v1e{&l&rP&)=1l*2cnH*mjHq@B<&-6+tRAZRC<~DwV;-yAF+&F~Krxk)upAO$G*?zjK)r;wvteuwFu>cf6nQ^db~PCHiDN}boyMy z`1tFE*}r@|MoQvd1ImF}D9Gyi%OS6l$+9Y0Nm*}j3AjZR z?ug*4xO*HY0xtbM=K<|QNS)st2MdN?+BkZyW3L=Cd+^TWA=_WD`whXu~9cV^sU(%jG8E7sEHqQ{ky|p5a z`}5Bc0Q(ovGvID*heiF#AwIYU0XjyqoZAnpYtwaQCLwARB z34Za5J^c3i>OB)P9;@ff;wD-U#dN=T;eu@MFB4z@sdW<0H1SG(&P6zm1^Y;l&Nk1o z2zEDJSQJB|Bn9F{(s2Og$c6c+^G?Gz16}hRD0KJ$DeJ3cSE_gSLOr8?Ra36G(F!aInGZqlW#(dRB_fFEV}kQZ;n=Qz#@I zmX(0xItvY?!rJa^1`k!H_i%B3*HxVqiFYe7T^%Wk4KQ;gpvvzn!A-(<#LP=X%g9zZ z{lZ9H?}C*&djPsFdP^VLV8m^aC{L+ zCWrm79zTP{E6~IxG58OCQ_3-m`xzy=+(I2?2BI5wL%zcro|Bs6)(lb=*9Al@rfG$zN^rM)@--{ zHqW7HJPq@^JXxhUs)#dg5&UwIqP2-Wepur+^ve}{y)x6@SHcnEP>Q( zPUz(o&Q*F&UNo=&ZnFIj`vd=)E?Mu#ilrFE#2?}d^{MTiw&y%_hW)g0ag6%Cl9vAr)_rlj(H20o<)%errk|gm?YsQ&{`5IR2Y^Xp*$7ne|DO{V zLO#!b3GpWYAEMO(?{J)wm}-OqBz z?yiE8AI^_yDHYgRqrG$_qklKvHLv#kdhzCnI^5q{ycksBRR@E7!r>IoKqM*g`PDz) z3RN^owLJIDvlm|-Z8shr5I6!Ci>&Lm2p4u{&|Z~*GCN0C>c$}<)Ko=X!Cn=7;QtmC zKHxfc31C;qt`!*L76tGVP&m-BXhC0`14Ix+ZrvqXU!tfSt%T+&0x|Du50D2hb4#55 z4c#);SdXmd&+|zlkkKoL?knHwUAcs9!w8t4eoJ+1k_kZYJ`o;wltiCap>aV9#L#(b z|L=1f9z;jWEVXDNxw+w4ywwFaUf}cD3BH%m&CX=meS9jLC}u2Xbo`_!>p9@{M+4+p zbLL}<6>E;c{&}L53{27AY^Jvyvl7>fK>|IRcNKNa^n%h#Bu{Y7d1^URLMvynP10!U zzKMtKWv56vzcJjjzqC?M|6>87<>6nN=o-Bii!E6lC*cT)ThU{SdW(OERp$YhH1WHt z7qq(h;p!5jA`9cqcJyQ0NRkM%1PEt5v^beV7)Czay^mr%?~(qh269J(l_fCpT?ZZQ zX`u7GRJ6il>wm)B1V$KvZZ-|gj|{v^hphH8k#yf?(QS{0U14uh&16h!QwxHd7oEG( zmlu!oB9x`Ael}Se;W4>fv>d*?S}BWLx-*#l(Gr-wv5BkvdC5XCkugqzf0@_+V=3F$ z#eu+TR=R?Zhpj?6FFSD>Ecy+<;?6BA>vCGh*7Z|#b_la$E=hf1UdM^AMn zcejcTDym%Xq|3%=Hg%}a9=6`$UT+oh6~%eBV)C$-aTbn((41(!+DTUnDUx|hd2);r z3oCmlj^=%b{57G9Ul8jo6NB|HH3NtdEuY9wLadC88+QAsCu%rRJZ4CZ^|^?E#{B&iZ)k(t3=8$sTN!#fPqfQ(2LD){JUk~f zk?}WzeQd$s*SF?s@74IRJIGTKokWcuttsm8FL7OO!YGP(noA^46CbbRO<%@%mD@ek-o{Iv%+P#Gd^!_FJb+imi4PW{%zR zS0iI9h1u~PZEDL70=IUO0KYdn82y<4Vm_x__wOU0KscKua)C_P3doD!^)QR0b=40h z>R{!$H7OA5h?<4*6a=DKne?oTbkD#1f=g6_#L3sJ3k1eRF0?`IUI#s=Kc}tZe-T$R z);z(5(fBak#;fv#h~Sc^fV{zAMvoV#NM+J1zAt;Q!dVugv8#G#?ol8dXVA{T6L2Mg zn1qbNDf?(^*yE*qS55gk+uw72LT6Enef$A$_|xqm%5wm?`i&RtVV|m(Ch6ZZ zpkB0_rhCUYM4$Fnod=lB4hmpg`5ptk{JF{r-;yTJzsK+y;Rk!auXcX-LKfX;o~!5z?3ct7kzR9cz>Oo=(YEDdOwb^&bm?89TlDW@)jhY_!t z)i&G32pQ$WA<)g@yyc~z*zbS{*RIc5P%T8P^R(X!a@S(WVF5_y`k~X((u>C9P@4Rw$pUY&6CL-uEX z2PyGq`yfMyJ&1tqooUBtl1&i&M=~x061T72BV}V0JV|xhN)=5%z;Udc(mfc=f=(Pf z-!HOP@xd?{n31|Q8RUiBRfhT^A;|c1*2G~&F~2}H5faGyAeO*55TGP^Z@3J zc5w1TRAsI7?gn^wWL(lF)dFX;RLpP_Hj}@uWDlUOMc&@~;=UIQ?O2v7fjr=9;^2&nSlyt5hPTuvk(X+2$`>Y3(ViMLcQmhg8TM$v;a<@ z*~tg!pl$DXTasd}QV0x?M=K8X{tR0fTZV44ptX%h;mq1?)M`u(3i%kp?PKcu&)L(! z+1JO*yG{632Aei`P3!yunc64=pxQ2QhwttH%tkpg`~6iYg;lPBoZt5*2TW!afpEr- z(}z*$JUVm?M_no-GQk(CbC&p7;izAr3-@Eumxf0qB>F zVfH85puMdZn;_(Sf4d!BKrB|k?)%im+^l1(u$=&xwt*9`2%E3AS};x z)H&aE2RrI+;2WbzE^y4P!(k|jV3>Gi3oJYln4viN!G;y`_)h%t4HF?n4d$Em9z=~3G{!%Y#_Rn~>lOW+?`KHY++YE^NOQ~6#(8Lb z{9VqjFDJTz_|M6de}4!= z)HCQiYIave|6UC>G#y&}?_EPUA(J*uw=E)3tr-PplX)s=Xfj+0Kl@LIx2S`gpqLt$ z*!~gcqDIQeBq~T}h2;G4!hmr}=I@zFgrH3_X)3c|^RBtK=!5(pnD+7{t@ z{O@;#2Edi8>v;I?pO^a2Ra#W%6^Oe;O+-X=Li@bB5!!M>mULtSbH7KimFCjT-?He& zC0B+2{?hNExFcdBqNAf5sZ`qVO=N^`A|ZO?lHa4ur}`WI50UnTE9Z+f&|&`%E70~i literal 0 HcmV?d00001 diff --git a/docs/img/fed_stack_later.png b/docs/img/fed_stack_later.png new file mode 100644 index 0000000000000000000000000000000000000000..42a2edf1a15ddc3d19db82fd043a38061309dc32 GIT binary patch literal 91311 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%0DFoDKM(} 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 H5%T{4`(-#c literal 0 HcmV?d00001 diff --git a/mkdocs.yml b/mkdocs.yml index 1e901d3c..1faf60e8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,6 +14,7 @@ nav: - Basic concepts: basic_idea.md - Overall architecture: overall_architecture.md - Schemas: schemas.md + - Federation: federation.md - Parsers: parsers.md - Detectors: detectors.md - Alert Aggregation: alert_aggregator.md From 68fdda79947c4565416a7ee10c105ad45af92d85 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 17 Aug 2026 12:26:52 +0200 Subject: [PATCH 08/10] add automated examples in federated docs --- docs/examples/others/federation.py | 88 ++++++++++++++++++++++++++++++ docs/federation.md | 79 +++------------------------ 2 files changed, 96 insertions(+), 71 deletions(-) create mode 100644 docs/examples/others/federation.py diff --git a/docs/examples/others/federation.py b/docs/examples/others/federation.py new file mode 100644 index 00000000..8e21ffa2 --- /dev/null +++ b/docs/examples/others/federation.py @@ -0,0 +1,88 @@ + +# --8<-- [start:example_1] +from detectmatelibrary.common.core import CoreComponent +import struct + + +class NewComponent(CoreComponent): # Inherent from CoreComponent + def __init__(self, elems): + self.elems = elems + super().__init__(name="FedExample") + + def aggregate_strategy(self, components): + final_list = [] + for component in components: + final_list.extend(component.elems) + + final_list = list(set(final_list)) + for component in components: + component.elems = final_list + + def to_binary(self): + return struct.pack(f">{len(self.elems)}h", *self.elems) + + def from_binary(self, binary): + num_ints = len(binary) // 2 + elems = list(struct.unpack(f">{num_ints}h", binary)) + return NewComponent(elems=elems) + + +# --8<-- [end:example_1] +# --8<-- [start:example_2] +detector1 = NewComponent([1, 2, 3]) +detector2 = NewComponent([4, 5]) +detector3 = NewComponent([6]) + +detector1 + detector2 + detector3 + +detector2.aggregate() # Detector 2 is used as centralize node + +print("Dectector 3", detector3.elems) # All detectors have been updated + +# --8<-- [end:example_2] + +# --8<-- [start:example_3] +detector1 = NewComponent([1, 2, 3]) +detector2 = NewComponent([4, 5]) +detector3 = NewComponent([6]) + +detector1 + detector2 + detector3 + +detector2.aggregate() # Detector 2 is used as centralize node + +print("Dectector 3", detector3.elems) # All detectors have been updated +# --8<-- [end:example_3] + +# --8<-- [start:example_4] +detector1 = NewComponent([1, 2, 3]) +detector2 = NewComponent([4, 5]) +detector3 = NewComponent([6]) + +detector1.stack([detector2, detector3]) + +detector2.aggregate() +print("Detector 3", detector3.elems) # It is not longer combine but stack, so it will not work + +detector1.aggregate() +print("Detector 3", detector3.elems) # Now it will work + +# --8<-- [end:example_4] + +# --8<-- [start:example_5] +detector1 = NewComponent([1, 2, 3]) + +binary2 = NewComponent([4, 5]).to_binary() +binary3 = (detector3 := NewComponent([6])).to_binary() +print("Binary of Detector 3", binary3) + +detector1.stack([binary2, binary3]) +output = detector1.aggregate(unstack=True) # unstack = True will free memory + +print("Detector 1", detector1.elems) # Detector 1 has been updated +print("Output binary", output) # Output that we send to other componets + +# We update detector 3 now +print("Detector 3", detector3.elems) # Now detector 3 is not in the share memory so it will not work +detector3 = detector3.from_binary(output) +print("Detector 3", detector3.elems) # Now it will work +# --8<-- [end:example_5] diff --git a/docs/federation.md b/docs/federation.md index dcc5e069..b19459ca 100644 --- a/docs/federation.md +++ b/docs/federation.md @@ -23,30 +23,7 @@ There are two man ways to use federation: For all the examplaes bellow, we will use this code: ```python -import struct - - -class NewComponent(CoreComponent): # Inherent from CoreComponent - def __init__(self, elems): - self.elems = elems - super().__init__() - - def aggregate_strategy(self, components): - final_list = [] - for component in components: - final_list.extend(component.elems) - - final_list = list(set(final_list)) - for component in components: - component.elems = final_list - - def to_binary(self): - return struct.pack(f">{len(self.elems)}h", *self.elems) - - def from_binary(self, binary): - num_ints = len(binary) // 2 - elems = list(struct.unpack(f">{num_ints}h", binary)) - return NewComponent(elems=elems) +--8<-- "docs/examples/others/federation.py:example_1" ``` @@ -58,32 +35,15 @@ The diagram bellow show the workflow: Example 1: -```python -detector1 = NewComponent([1, 2, 3]) -detector2 = NewComponent([4, 5]) -detector3 = NewComponent([6]) - -detector1 + detector2 + detector3 - -detector2.aggregate() # Detector 2 is used as centralize node -print("Dectector 3", detector3.elems) # All detectors have been updated +```python +--8<-- "docs/examples/others/federation.py:example_2" ``` Example 2: ```python -detector1 = NewComponent([1, 2, 3]) -detector2 = NewComponent([4, 5]) -detector3 = NewComponent([6]) - -(detector1 + detector2 + detector3) - detector2 - -detector2.aggregate() # Detector 2 is used as centralize node -print("Detector 3", detector3.elems) # Still the same - -detector1.aggregate() # Detector 1 is used as centralize node -print("Detector 3", detector3.elems) # Detector 3 has been updated now +--8<-- "docs/examples/others/federation.py:example_3" ``` ## Stack @@ -93,36 +53,13 @@ The diagram bellow show the workflow: ![combine](img/fed_stack_later.png) Example 1: -```python -detector1 = NewComponent([1, 2, 3]) -detector2 = NewComponent([4, 5]) -detector3 = NewComponent([6]) -detector1.stack([detector2, detector3]) - -detector2.aggregate() -print("Detector 3", detector3.elems) # It is not longer combine but stack, so it will not work - -detector1.aggregate() -print("Detector 3", detector3.elems) # Now it will work +```python +--8<-- "docs/examples/others/federation.py:example_4" ``` Example 2: -```python -detector1 = NewComponent([1, 2, 3]) - -binary2 = NewComponent([4, 5]).to_binary() -binary3 = (detector3 := NewComponent([6])).to_binary() -print("Binary of Detector 3", binary3) - -detector1.stack([binary2, binary3]) -output = detector1.aggregate(unstack=True) # unstack = True will free memory -print("Detector 1", detector1.elems) # Detector 1 has been updated -print("Output binary", output) # Output that we send to other componets - -# We update detector 3 now -print("Detector 3", detector3.elems) # Now detector 3 is not in the share memory so it will not work -detector3 = detector3.from_binary(output) -print("Detector 3", detector3.elems) # Now it will work +```python +--8<-- "docs/examples/others/federation.py:example_5" ``` From 0faee06821b3f2f67e5dafae3f1cfec34214db6a Mon Sep 17 00:00:00 2001 From: ipmach Date: Mon, 17 Aug 2026 12:48:04 +0200 Subject: [PATCH 09/10] Improve clarity and fix typos in federation.md Updated text for clarity and corrected typos in the federation documentation. --- docs/federation.md | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/federation.md b/docs/federation.md index b19459ca..d531b5a4 100644 --- a/docs/federation.md +++ b/docs/federation.md @@ -1,39 +1,37 @@ # Federation -In this section, we will explain how to use the federation setup. For a component to use federation needs to have implemented the next methods. +This section explains how to use the federation setup. For a component to support federation, it must implement the following methods: ```python - def to_binary(self) -> bytes | None: - """(Federation only) fill it to be compatible with federation ops""" +def to_binary(self) -> bytes | None: + """(Federation only) Serialize to bytes for federation operations.""" - def from_binary(self, binary: bytes) -> object: - """(Federation only) fill it to be compatible with federation ops""" +def from_binary(self, binary: bytes) -> object: + """(Federation only) Deserialize from bytes for federation operations.""" - def aggregate_strategy(self, components: set["FedOperations"]) -> None: - """(Federation only) fill it to be compatible with federation ops""" +def aggregate_strategy(self, components: set["FedOperations"]) -> None: + """(Federation only) Define how to aggregate a set of federated components.""" ``` -There are two man ways to use federation: +There are two main ways to use federation: -* **Combine first**: only can be use when all components run locally. The main idea is to simplify the process by allowing components to share memory. -* **Stack later**: a more standard approach to federated where the "weights" of each component are combine at the end. +- **Combine first**: This can only be used when all components run locally. The main idea is to simplify the process by allowing components to share memory. +- **Stack later**: A more standard federated approach where the "weights" or state of each component are combined at the end. ## Example class -For all the examplaes bellow, we will use this code: +For all the examples below, we will use this code: ```python --8<-- "docs/examples/others/federation.py:example_1" ``` - ## Combine first -The diagram bellow show the workflow: +The diagram below shows the workflow: ![combine](img/fed_combine_first.png) - Example 1: ```python @@ -48,9 +46,9 @@ Example 2: ## Stack -The diagram bellow show the workflow: +The diagram below shows the workflow: -![combine](img/fed_stack_later.png) +![stack](img/fed_stack_later.png) Example 1: From 6be9eb56e5c1e91c4db6b6ee15fc352b00d0a8e5 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Wed, 26 Aug 2026 09:56:26 +0200 Subject: [PATCH 10/10] address comments --- .../common/_core_op/_fed_component.py | 14 +++++++++++--- tests/test_common/test_core_federation.py | 6 +++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/detectmatelibrary/common/_core_op/_fed_component.py b/src/detectmatelibrary/common/_core_op/_fed_component.py index 04c40be1..eef4900d 100644 --- a/src/detectmatelibrary/common/_core_op/_fed_component.py +++ b/src/detectmatelibrary/common/_core_op/_fed_component.py @@ -3,7 +3,7 @@ from typing import Self, overload -class IncompabtibleFed(Exception): +class IncompatibleFed(Exception): def __init__(self) -> None: super().__init__("Instances are incompatible") @@ -12,7 +12,7 @@ class _CompOp: @staticmethod def is_compatible(main_inst: object, other_inst: object) -> None: if not isinstance(other_inst, type(main_inst)): - raise IncompabtibleFed() + raise IncompatibleFed() @staticmethod def reset(main_inst: object, attr: str) -> None: @@ -47,6 +47,7 @@ def stack(main_inst: object, attr: str, list_other_inst: list[object]) -> None: class FedOperations: + """Operations related to the federation learning / agregation.""" __COMPONENT: str = "_components" def __init__(self) -> None: @@ -54,11 +55,15 @@ def __init__(self) -> None: _CompOp.reset(self, self.__COMPONENT) def __add__(self, other: object) -> Self: + """Add other components to do the aggregation in a combine first + approach.""" _CompOp.combine(self, attr=self.__COMPONENT, other_inst=other) return self def __sub__(self, other: object) -> Self: + """Remove other components to do the aggregation in a combine first + approach.""" _CompOp.uncombine(self, attr=self.__COMPONENT, other_inst=other) return self @@ -69,6 +74,8 @@ def stack(self, other: bytes | list[bytes]) -> None: @overload def stack(self, other: object | list[object]) -> None: + """Stack other components to do the aggregation in a stack later + approach.""" pass def stack(self, other: object | list[object | bytes] | bytes) -> None: @@ -93,8 +100,9 @@ def to_binary(self) -> bytes | None: return None def from_binary(self, binary: bytes) -> object: - warnings.warn(f"To binary not implemented, return None for {binary!r}") + warnings.warn(f"From binary not implemented, return None for {binary!r}") return None def aggregate_strategy(self, components: set["FedOperations"]) -> None: + """Aggregation strategy use by the component.""" warnings.warn(f"No strategy found, aggregations does nothing for {components}") diff --git a/tests/test_common/test_core_federation.py b/tests/test_common/test_core_federation.py index 66627033..826fe406 100644 --- a/tests/test_common/test_core_federation.py +++ b/tests/test_common/test_core_federation.py @@ -1,4 +1,4 @@ -from detectmatelibrary.common._core_op._fed_component import IncompabtibleFed +from detectmatelibrary.common._core_op._fed_component import IncompatibleFed from detectmatelibrary.common.core import CoreComponent import struct @@ -43,9 +43,9 @@ def test_incompatible(self) -> None: component1 = DummyComponent2(name="comp_1") component2 = DummyComponent(name="comp_2") - with pytest.raises(IncompabtibleFed): + with pytest.raises(IncompatibleFed): component1 + component2 - with pytest.raises(IncompabtibleFed): + with pytest.raises(IncompatibleFed): component1 - component2 def test_stack(self) -> None: