diff --git a/CMakeLists.txt b/CMakeLists.txt
index a0d70035e0e150ec33dc4806bd02632debbf0a42..ec6aacd723a50eba2bfed0184941410340c6a7aa 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -11,6 +11,7 @@ set(module_name _${project}) # target name
 
 
 project(${project})
+set(CXX_STANDARD 14)
 
 ##############################################
 # Define options
@@ -18,6 +19,7 @@ option(PYBIND "python binding" ON)
 option(WERROR "Warning as error" OFF)
 option(TEST "Enable tests" ON)
 option(COVERAGE "Enable coverage" OFF)
+option(ENABLE_ASAN "Enable ASan (AddressSanitizer) for runtime analysis of memory use (over/underflow, memory leak, ...)" OFF)
 
 ##############################################
 # Import utils CMakeLists
@@ -30,8 +32,19 @@ endif()
 
 ##############################################
 # Find system dependencies
+Include(FetchContent)
 
+FetchContent_Declare(
+    fmt
+    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
+    GIT_TAG        10.2.1 # or a later release
+)
+
+set(FMT_SYSTEM_HEADERS ON)
+FetchContent_MakeAvailable(fmt)
+set_property(TARGET fmt PROPERTY POSITION_INDEPENDENT_CODE ON)
 
+find_package(Threads REQUIRED)
 
 ##############################################
 # Create target and set properties
@@ -51,6 +64,19 @@ target_include_directories(${module_name}
         ${CMAKE_CURRENT_SOURCE_DIR}/src
 )
 
+if( ${ENABLE_ASAN} )
+    message("Building ${module_name} with ASAN.")
+    set(SANITIZE_FLAGS -fsanitize=address -fno-omit-frame-pointer)
+    target_link_libraries(${module_name}
+        PUBLIC
+            -fsanitize=address
+    )
+    target_compile_options(${module_name}
+        PRIVATE
+            ${SANITIZE_FLAGS}
+    )
+endif()
+
 # PYTHON BINDING
 if (PYBIND)
     generate_python_binding(${project} ${module_name})
@@ -64,6 +90,7 @@ if (PYBIND)
         )
 endif()
 
+target_link_libraries(${module_name} PUBLIC Threads::Threads fmt::fmt)
 target_compile_features(${module_name} PRIVATE cxx_std_14)
 
 if (DOSANITIZE STREQUAL "ON")
diff --git a/aidge_core-config.cmake.in b/aidge_core-config.cmake.in
index adfbf2838bdbba48c7c2e8420fece43054cd39d3..d97afe8a2a1ca98eb862d66c388081bca7b72edc 100644
--- a/aidge_core-config.cmake.in
+++ b/aidge_core-config.cmake.in
@@ -1,5 +1,9 @@
 @PACKAGE_INIT@
 
+include(CMakeFindDependencyMacro)
+find_dependency(fmt)
+find_dependency(Threads)
+
 include(${CMAKE_CURRENT_LIST_DIR}/aidge_core-config-version.cmake)
 
 include(${CMAKE_CURRENT_LIST_DIR}/aidge_core-targets.cmake)
diff --git a/aidge_core/export/node_export.py b/aidge_core/export/node_export.py
index bea61551d6b4363d234fba4df6138ccef3154331..477989b037da6f6229bd275ff22974d9ef307848 100644
--- a/aidge_core/export/node_export.py
+++ b/aidge_core/export/node_export.py
@@ -39,7 +39,11 @@ class ExportNode(ABC):
             if parent_node is not None:
                 self.inputs_dims.append(self.operator.get_input(idx).dims())
             else:
-                self.inputs_dims.append(None)
+                print(self.operator.get_input(idx))
+                if self.operator.get_input(idx) is not None:
+                    self.inputs_dims.append(self.operator.get_input(idx).dims())
+                else:
+                    self.inputs_dims.append(None)
 
         for idx, child_node in enumerate(self.node.get_children()):
             self.outputs.append(child_node)
diff --git a/aidge_core/unit_tests/test_impl.py b/aidge_core/unit_tests/test_impl.py
new file mode 100644
index 0000000000000000000000000000000000000000..4aacfafd7d51830dc89b7b30ea5ebf521a13fe30
--- /dev/null
+++ b/aidge_core/unit_tests/test_impl.py
@@ -0,0 +1,72 @@
+"""
+Copyright (c) 2023 CEA-List
+
+This program and the accompanying materials are made available under the
+terms of the Eclipse Public License 2.0 which is available at
+http://www.eclipse.org/legal/epl-2.0.
+
+SPDX-License-Identifier: EPL-2.0
+"""
+
+import unittest
+import aidge_core
+from functools import reduce
+
+import numpy as np
+
+GLOBAL_CPT = 0
+
+class testImpl(aidge_core.OperatorImpl):
+    def __init__(self, op: aidge_core.Operator):
+        aidge_core.OperatorImpl.__init__(self, op, 'cpu') # Required to avoid type error !
+
+    def forward(self):
+        global GLOBAL_CPT
+        GLOBAL_CPT += 1
+
+class test_OperatorImpl(unittest.TestCase):
+    """Test Op
+    """
+    def setUp(self):
+        global GLOBAL_CPT
+        GLOBAL_CPT = 0
+    def tearDown(self):
+        pass
+
+    def test_setImplementation(self):
+        """Test setting an implementation manually
+        """
+        global GLOBAL_CPT
+        matmul = aidge_core.GenericOperator("MatMul", 1, 0, 1, name="MatMul0")
+        generic_matmul_op = matmul.get_operator()
+        generic_matmul_op.set_compute_output_dims(lambda x: x)
+        generic_matmul_op.set_impl(testImpl(generic_matmul_op))
+        generic_matmul_op.forward()
+        self.assertEqual(GLOBAL_CPT, 1)
+
+    def test_Registrar_setOp(self):
+        """Test registering an implementation
+        """
+        global GLOBAL_CPT
+        aidge_core.register_ConvOp2D("cpu", testImpl)
+        self.assertTrue("cpu" in aidge_core.get_keys_ConvOp2D())
+        conv = aidge_core.Conv2D(2,2,[1,1], name="Conv0")
+        conv.get_operator().set_backend("cpu")
+        conv.get_operator().forward()
+        self.assertEqual(GLOBAL_CPT, 1)
+
+    def test_Registrar_setGraphView(self):
+        """Test registering an implementation
+        """
+        global GLOBAL_CPT
+        aidge_core.register_ConvOp2D("cpu", testImpl)
+        aidge_core.register_ProducerOp("cpu", testImpl)
+        self.assertTrue("cpu" in aidge_core.get_keys_ConvOp2D())
+        conv = aidge_core.Conv2D(2,2,[1,1], name="Conv0")
+        model = aidge_core.sequential([conv])
+        model.set_backend("cpu")
+        conv.get_operator().forward()
+        self.assertEqual(GLOBAL_CPT, 1)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/aidge_core/unit_tests/test_operator_binding.py b/aidge_core/unit_tests/test_operator_binding.py
index 825ca6100382116443699a00bcff27b9bbca028a..c94960733b24444218b1209463adbda11b89f6e8 100644
--- a/aidge_core/unit_tests/test_operator_binding.py
+++ b/aidge_core/unit_tests/test_operator_binding.py
@@ -108,7 +108,7 @@ class test_operator_binding(unittest.TestCase):
             """Dummy implementation to test that C++ call python code
             """
             def __init__(self, op: aidge_core.Operator):
-                aidge_core.OperatorImpl.__init__(self, op) # Recquired to avoid type error !
+                aidge_core.OperatorImpl.__init__(self, op, 'test_impl') # Recquired to avoid type error !
                 self.idx = 0
 
             def forward(self):
@@ -120,11 +120,28 @@ class test_operator_binding(unittest.TestCase):
         generic_op = generic_node.get_operator()
         customImpl = PythonCustomImpl(generic_op)
 
-        generic_op.forward() # Do nothing, no implementation set
+        #generic_op.forward() # Throw an error, no implementation set
         generic_op.set_impl(customImpl)
         generic_op.forward() # Increment idx
         self.assertEqual(customImpl.idx, 1)
 
+    def test_magic_meth(self):
+        myVar = 2
+        myBool = True
+        # Test dynamic attribute set
+        gop = aidge_core.GenericOperator("test", 1, 0, 1, "FictiveName", myVar=myVar).get_operator()
+        gop.myBool = myBool
+        # Test variable set by kwargs
+        self.assertEqual(gop.myVar, myVar)
+        # Test set attr
+        self.assertEqual(gop.myBool, myBool)
+
+        # Test static attribute set !
+        prod = aidge_core.Producer([1]).get_operator()
+        self.assertEqual(prod.Constant, False)
+        prod.Constant = True # By default Constant is False
+        self.assertEqual(prod.Constant, True)
+
 
 
 if __name__ == '__main__':
diff --git a/aidge_core/unit_tests/test_parameters.py b/aidge_core/unit_tests/test_parameters.py
index 620beb160fb3494f156c1a4b512d386447081154..e4d2cb4faca3dda64cff6aea541c30787c23d0ad 100644
--- a/aidge_core/unit_tests/test_parameters.py
+++ b/aidge_core/unit_tests/test_parameters.py
@@ -39,12 +39,6 @@ class test_attributes(unittest.TestCase):
         self.assertEqual(fc_op.get_attr("OutChannels"), out_channels)
         self.assertEqual(fc_op.get_attr("NoBias"), nb_bias)
 
-    def test_matmul(self):
-        in_channels = 4
-        out_channels = 8
-        matmul_op = aidge_core.MatMul(in_channels, out_channels).get_operator()
-        self.assertEqual(matmul_op.get_attr("OutChannels"), out_channels)
-
     def test_producer_1D(self):
         dims = [5]
         producer_op = aidge_core.Producer(dims).get_operator()
diff --git a/aidge_core/unit_tests/test_recipies.py b/aidge_core/unit_tests/test_recipes.py
similarity index 95%
rename from aidge_core/unit_tests/test_recipies.py
rename to aidge_core/unit_tests/test_recipes.py
index 26ae544d6e05f2f9a9da371d3617f9265a037364..240bcd9501aa1fd64985fa59c87f01dfdf9343aa 100644
--- a/aidge_core/unit_tests/test_recipies.py
+++ b/aidge_core/unit_tests/test_recipes.py
@@ -11,7 +11,7 @@ SPDX-License-Identifier: EPL-2.0
 import unittest
 import aidge_core
 
-class test_recipies(unittest.TestCase):
+class test_recipes(unittest.TestCase):
     """
     """
     def setUp(self):
@@ -45,9 +45,9 @@ class test_recipies(unittest.TestCase):
         self.assertTrue(all([i in old_nodes for i in graph_view.get_nodes()]))
 
     def test_fuse_matmul_add(self):
-        matmul0 = aidge_core.MatMul(1, 1, name="MatMul0")
+        matmul0 = aidge_core.MatMul(name="MatMul0")
         add0 = aidge_core.Add(2, name="Add0")
-        matmul1 = aidge_core.MatMul(1, 1, name="MatMul1")
+        matmul1 = aidge_core.MatMul(name="MatMul1")
         add1 = aidge_core.Add(2, name="Add1")
 
         graph_view = aidge_core.sequential([matmul0, add0, matmul1, add1])
diff --git a/aidge_core/unit_tests/test_tensor.py b/aidge_core/unit_tests/test_tensor.py
index a214a0e354c64b515d0a7ac24d81c85e116938ca..d479c98b20534daa804f6019b63d528883c2b568 100644
--- a/aidge_core/unit_tests/test_tensor.py
+++ b/aidge_core/unit_tests/test_tensor.py
@@ -10,16 +10,16 @@ SPDX-License-Identifier: EPL-2.0
 
 import unittest
 import aidge_core
-
 from functools import reduce
+
 import numpy as np
 
+
 class test_tensor(unittest.TestCase):
-    """
+    """Test tensor binding
     """
     def setUp(self):
         pass
-
     def tearDown(self):
         pass
 
@@ -35,10 +35,60 @@ class test_tensor(unittest.TestCase):
             idx = t.get_idx(coord)
             self.assertEqual(idx, i)
 
-if __name__ == '__main__':
-    unittest.main()
+    def test_getavailable_backends(self):
+        self.assertTrue("cpu" in aidge_core.Tensor.get_available_backends())
+
+    def test_numpy_int_to_tensor(self):
+        np_array = np.arange(9).reshape(1,1,3,3).astype(np.int32)
+        # Numpy -> Tensor
+        t = aidge_core.Tensor(np_array)
+        self.assertEqual(t.dtype(), aidge_core.DataType.Int32)
+        for i_t, i_n in zip(t, np_array.flatten()):
+            self.assertTrue(i_t == i_n)
+        for i,j in zip(t.dims(), np_array.shape):
+            self.assertEqual(i,j)
+    def test_tensor_int_to_numpy(self):
+        np_array = np.arange(9).reshape(1,1,3,3)
+        # Numpy -> Tensor
+        t = aidge_core.Tensor(np_array)
+        # Tensor -> Numpy
+        nnarray = np.array(t)
+        for i_nn, i_n in zip(nnarray.flatten(), np_array.flatten()):
+            self.assertTrue(i_nn == i_n)
+        for i,j in zip(t.dims(), nnarray.shape):
+            self.assertEqual(i,j)
 
+    def test_numpy_int64_to_tensor(self):
+        np_array = np.arange(9).reshape(1,1,3,3).astype(np.int64)
+        # Numpy -> Tensor
+        t = aidge_core.Tensor(np_array)
+        self.assertEqual(t.dtype(), aidge_core.DataType.Int64)
+        for i_t, i_n in zip(t, np_array.flatten()):
+            self.assertTrue(i_t == i_n)
+        for i,j in zip(t.dims(), np_array.shape):
+            self.assertEqual(i,j)
 
+    def test_numpy_float_to_tensor(self):
+        t = aidge_core.Tensor()
+        np_array = np.random.rand(1, 1, 3, 3).astype(np.float32)
+        # Numpy -> Tensor
+        t = aidge_core.Tensor(np_array)
+        self.assertEqual(t.dtype(), aidge_core.DataType.Float32)
+        for i_t, i_n in zip(t, np_array.flatten()):
+            self.assertTrue(i_t == i_n) # TODO : May need to change this to a difference
+        for i,j in zip(t.dims(), np_array.shape):
+            self.assertEqual(i,j)
 
+    def test_get_set(self):
+        dims = [2,2,2]
 
+        np_array = np.arange(8).reshape(dims).astype(np.int32)
+        # Numpy -> Tensor
+        t = aidge_core.Tensor(np_array)
+        for i in range(8):
+            self.assertEqual(t[i], i)
+            t[i] = 5
+            self.assertEqual(t[i], 5)
 
+if __name__ == '__main__':
+    unittest.main()
diff --git a/include/aidge/aidge.hpp b/include/aidge/aidge.hpp
index cc0979b07b07c2b95515eda09fda68a9ec4ac63e..931b1b26a04e8886c211d77f8b0147c2140d350a 100644
--- a/include/aidge/aidge.hpp
+++ b/include/aidge/aidge.hpp
@@ -1,72 +1,80 @@
-/********************************************************************************
- * Copyright (c) 2023 CEA-List
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * SPDX-License-Identifier: EPL-2.0
- *
- ********************************************************************************/
-
-#ifndef AIDGE_IMPORTS_H_
-#define AIDGE_IMPORTS_H_
-
-#include "aidge/backend/OperatorImpl.hpp"
-#include "aidge/backend/TensorImpl.hpp"
-
-#include "aidge/data/Data.hpp"
-#include "aidge/data/Tensor.hpp"
-
-#include "aidge/graph/Connector.hpp"
-#include "aidge/graph/GraphView.hpp"
-#include "aidge/graph/Node.hpp"
-#include "aidge/graph/OpArgs.hpp"
-#include "aidge/graphmatching/Match.hpp"
-#include "aidge/graphmatching/NodeRegex.hpp"
-#include "aidge/graphmatching/SeqStm.hpp"
-#include "aidge/graphmatching/StmFactory.hpp"
-#include "aidge/graphmatching/Utile.hpp"
-
-#include "aidge/operator/Add.hpp"
-#include "aidge/operator/AvgPooling.hpp"
-#include "aidge/operator/BatchNorm.hpp"
-#include "aidge/operator/Concat.hpp"
-#include "aidge/operator/Conv.hpp"
-#include "aidge/operator/ConvDepthWise.hpp"
-#include "aidge/operator/Div.hpp"
-#include "aidge/operator/Erf.hpp"
-#include "aidge/operator/FC.hpp"
-#include "aidge/operator/Gather.hpp"
-#include "aidge/operator/GenericOperator.hpp"
-#include "aidge/operator/MatMul.hpp"
-#include "aidge/operator/MaxPooling.hpp"
-#include "aidge/operator/MetaOperator.hpp"
-#include "aidge/operator/MetaOperatorDefs.hpp"
-#include "aidge/operator/Mul.hpp"
-#include "aidge/operator/Operator.hpp"
-#include "aidge/operator/Pad.hpp"
-#include "aidge/operator/Producer.hpp"
-#include "aidge/operator/Pow.hpp"
-#include "aidge/operator/ReduceMean.hpp"
-#include "aidge/operator/ReLU.hpp"
-#include "aidge/operator/Reshape.hpp"
-#include "aidge/operator/Scaling.hpp"
-#include "aidge/operator/Slice.hpp"
-#include "aidge/operator/Softmax.hpp"
-#include "aidge/operator/Sqrt.hpp"
-#include "aidge/operator/Sub.hpp"
-#include "aidge/operator/Transpose.hpp"
-#include "aidge/scheduler/Scheduler.hpp"
-
-#include "aidge/recipies/Recipies.hpp"
-
-#include "aidge/utils/Attributes.hpp"
-#include "aidge/utils/StaticAttributes.hpp"
-#include "aidge/utils/DynamicAttributes.hpp"
-#include "aidge/utils/Registrar.hpp"
-#include "aidge/utils/Types.h"
-//#include "aidge/utilsParsing/AstNode.hpp"
-//#include "aidge/utilsParsing/ParsingToken.hpp"
-
-#endif /* AIDGE_IMPORTS_H_ */
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_IMPORTS_H_
+#define AIDGE_IMPORTS_H_
+
+#include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/backend/TensorImpl.hpp"
+#include "aidge/backend/StimulusImpl.hpp"
+
+#include "aidge/backend/cpu/data/TensorImpl.hpp"
+#include "aidge/backend/cpu/data/GetCPUPtr.h"
+
+#include "aidge/data/Data.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/data/Database.hpp"
+#include "aidge/data/DataProvider.hpp"
+
+#include "aidge/graph/Connector.hpp"
+#include "aidge/graph/GraphView.hpp"
+#include "aidge/graph/Node.hpp"
+#include "aidge/graph/OpArgs.hpp"
+
+#include "aidge/graphRegex/GraphRegex.hpp"
+
+#include "aidge/filler/Filler.hpp"
+
+#include "aidge/nodeTester/ConditionalInterpreter.hpp"
+
+#include "aidge/operator/Add.hpp"
+#include "aidge/operator/AvgPooling.hpp"
+#include "aidge/operator/BatchNorm.hpp"
+#include "aidge/operator/Concat.hpp"
+#include "aidge/operator/Conv.hpp"
+#include "aidge/operator/ConvDepthWise.hpp"
+#include "aidge/operator/Div.hpp"
+#include "aidge/operator/Erf.hpp"
+#include "aidge/operator/FC.hpp"
+#include "aidge/operator/Gather.hpp"
+#include "aidge/operator/GenericOperator.hpp"
+#include "aidge/operator/GlobalAveragePooling.hpp"
+#include "aidge/operator/MatMul.hpp"
+#include "aidge/operator/MaxPooling.hpp"
+#include "aidge/operator/MetaOperator.hpp"
+#include "aidge/operator/MetaOperatorDefs.hpp"
+#include "aidge/operator/Mul.hpp"
+#include "aidge/operator/Operator.hpp"
+#include "aidge/operator/Pad.hpp"
+#include "aidge/operator/Producer.hpp"
+#include "aidge/operator/Pow.hpp"
+#include "aidge/operator/ReduceMean.hpp"
+#include "aidge/operator/ReLU.hpp"
+#include "aidge/operator/Reshape.hpp"
+#include "aidge/operator/Scaling.hpp"
+#include "aidge/operator/Slice.hpp"
+#include "aidge/operator/Softmax.hpp"
+#include "aidge/operator/Sqrt.hpp"
+#include "aidge/operator/Sub.hpp"
+#include "aidge/operator/Transpose.hpp"
+#include "aidge/scheduler/Scheduler.hpp"
+#include "aidge/stimuli/Stimulus.hpp"
+
+#include "aidge/recipes/Recipes.hpp"
+
+#include "aidge/utils/Attributes.hpp"
+#include "aidge/utils/StaticAttributes.hpp"
+#include "aidge/utils/DynamicAttributes.hpp"
+#include "aidge/utils/Random.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
+
+#endif /* AIDGE_IMPORTS_H_ */
diff --git a/include/aidge/backend/OperatorImpl.hpp b/include/aidge/backend/OperatorImpl.hpp
index 19f0837504016f38ae96dd852bc6fa41b5ab53ba..6a9056723df133fef62e56f969d39d8f69390a76 100644
--- a/include/aidge/backend/OperatorImpl.hpp
+++ b/include/aidge/backend/OperatorImpl.hpp
@@ -9,23 +9,27 @@
  *
  ********************************************************************************/
 
-#ifndef AIDGE_OPERATORIMPL_H_
-#define AIDGE_OPERATORIMPL_H_
+#ifndef AIDGE_BACKEND_OPERATORIMPL_H_
+#define AIDGE_BACKEND_OPERATORIMPL_H_
 
-#include <cstddef>
+#include <string>
 #include <vector>
-#include <memory>
+
 #include "aidge/utils/Types.h"
+#include "aidge/data/Elts.hpp"
 
 namespace Aidge {
 class Operator;
 
 class OperatorImpl {
 public:
-    OperatorImpl(const Operator& op);
+    OperatorImpl(const Operator& op, const std::string& backend);
     virtual void forward();
     virtual void backward();
 
+    const std::string& backend() const noexcept {
+        return mBackend;
+    }
     /**
      * @brief Minimum amount of data from a specific input required by the
      * implementation to be run.
@@ -33,13 +37,13 @@ public:
      * @param inputIdx Index of the input analysed.
      * @return std::size_t
      */
-    virtual NbElts_t getNbRequiredData(const IOIndex_t inputIdx) const;
+    virtual Elts_t getNbRequiredData(const IOIndex_t inputIdx) const;
 
     // Amount of input data that cannot be overwritten during the execution.
-    virtual NbElts_t getNbRequiredProtected(const IOIndex_t inputIdx) const;
+    virtual Elts_t getNbRequiredProtected(const IOIndex_t inputIdx) const;
 
     // Memory required at an output for a given input size.
-    virtual NbElts_t getRequiredMemory(const IOIndex_t outputIdx, const std::vector<DimSize_t> &inputsSize) const;
+    virtual Elts_t getRequiredMemory(const IOIndex_t outputIdx, const std::vector<DimSize_t> &inputsSize) const;
 
     /**
      * @brief Total amount of consumed data from a specific input.
@@ -47,7 +51,7 @@ public:
      * @param inputIdx Index of the input analysed.
      * @return DimSize_t
      */
-    virtual NbElts_t getNbConsumedData(const IOIndex_t inputIdx) const;
+    virtual Elts_t getNbConsumedData(const IOIndex_t inputIdx) const;
 
     /**
      * @brief Total amount of produced data ready to be used on a specific output.
@@ -55,7 +59,7 @@ public:
      * @param outputIdx Index of the output analysed.
      * @return DimSize_t
      */
-    virtual NbElts_t getNbProducedData(const IOIndex_t outputIdx) const;
+    virtual Elts_t getNbProducedData(const IOIndex_t outputIdx) const;
 
     /**
      * @brief Update the Consummer Producer system by simulating the consumption and production of i/o
@@ -63,13 +67,20 @@ public:
      */
     virtual void updateConsummerProducer();
 
+    /**
+     * @brief Reset the Consummer Producer system.
+     *
+     */
+    virtual void resetConsummerProducer();
+
     virtual ~OperatorImpl() = default;
 
 protected:
     const Operator &mOp;
-    std::vector<NbElts_t> mNbConsumedData;
-    std::vector<NbElts_t> mNbProducedData;
+    const std::string mBackend;
+    std::vector<Elts_t> mNbConsumedData;
+    std::vector<Elts_t> mNbProducedData;
 };
 } // namespace Aidge
 
-#endif /* AIDGE_OPERATORIMPL_H_ */
+#endif /* AIDGE_BACKEND_OPERATORIMPL_H_ */
diff --git a/include/aidge/backend/StimulusImpl.hpp b/include/aidge/backend/StimulusImpl.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..fbdf57b1587d76160c0cb146b6fe9da6947541dc
--- /dev/null
+++ b/include/aidge/backend/StimulusImpl.hpp
@@ -0,0 +1,32 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CORE_BACKEND_STIMULUSIMPL_H_
+#define AIDGE_CORE_BACKEND_STIMULUSIMPL_H_
+
+#include <memory>
+
+#include "aidge/data/Tensor.hpp"
+
+namespace Aidge {
+
+/**
+ * @brief Base class to implement data loading functions.
+ */
+class StimulusImpl {
+public:
+    virtual ~StimulusImpl() noexcept = default;
+
+    virtual std::shared_ptr<Tensor> load() const = 0;
+};
+} // namespace Aidge
+
+#endif /* AIDGE_CORE_BACKEND_STIMULUSIMPL_H_ */
diff --git a/include/aidge/backend/TensorImpl.hpp b/include/aidge/backend/TensorImpl.hpp
index a27f0317c59916facef970a3c1b91704fb485cd4..f3fa4ef5164a2eed7caaa7baa7f83e7ed00403b8 100644
--- a/include/aidge/backend/TensorImpl.hpp
+++ b/include/aidge/backend/TensorImpl.hpp
@@ -12,8 +12,12 @@
 #ifndef AIDGE_TENSORIMPL_H_
 #define AIDGE_TENSORIMPL_H_
 
-#include <cstddef>
-#include <cstdio>
+#include <numeric>     // std::accumulate
+#include <cstddef>     // std::size_t
+#include <functional>  // std::multiplies
+#include <vector>
+#include <utility>     // std::pair, std::make_pair
+
 #include "aidge/data/Data.hpp"
 #include "aidge/utils/Types.h"
 #include "aidge/utils/ErrorHandling.hpp"
@@ -59,26 +63,42 @@ private:
 */
 
 /**
- * This class manages the raw data storage of a Tensor and provide generic copy
+ * @class TensorImpl
+ * @brief Class to manage the raw data storage of a Tensor and provide generic copy
  * primitives from other devices and from/to host.
- * It can own the data or not (use setRawPtr() to set an external data owner).
- * It only knows the data type and data capacity, but does not handle anything else.
+ * @note It can own the data or not (use ``setRawPtr()`` to set an external data owner).
+ * @note It only knows the data type and data capacity, but does not handle anything else.
 */
 class TensorImpl {
+protected:
+
+    const std::string mBackend;
+    /// @brief Device id.
+    const DeviceIdx_t mDevice;
+    /// Number of elements (to be) stored.
+    NbElts_t mNbElts;
+
 public:
     TensorImpl() = delete;
-    TensorImpl(const char *backend, DeviceIdx_t device = 0) : mBackend(backend), mDevice(device){};
 
-    /**
-     * Return the (backend, device) pair for this implementation.
-    */
-    std::pair<std::string, DeviceIdx_t> device() const { return std::make_pair(mBackend, mDevice); }
+    TensorImpl(const std::string& backend, DeviceIdx_t device, std::vector<DimSize_t> dims)
+        : mBackend(backend),
+          mDevice(device)
+    {
+        resize(dims);
+    };
 
+    virtual ~TensorImpl() = default;
+
+    virtual bool operator==(const TensorImpl &othImpl) const = 0;
+
+public:
     /**
-     * Set the device ID for current backend.
-     * @param device New device ID on current backend.
+     * Return the (backend, device) pair for this implementation.
     */
-    virtual void setDevice(DeviceIdx_t device) = 0;
+    std::pair<std::string, DeviceIdx_t> device() const noexcept {
+        return std::make_pair(mBackend, mDevice);
+    }
 
     /**
      * Copy data from the same device.
@@ -93,30 +113,34 @@ public:
      * @param srcDt Source data type.
      * @param src Pointer on current implementation device.
      * @param length Number of elements to copy.
+     * @param offset Destination offset (in number of elements).
     */
-    virtual void copyCast(const void *src, NbElts_t length, const DataType srcDt) = 0;
+    virtual void copyCast(const void *src, const DataType srcDt, NbElts_t length, NbElts_t offset = 0) = 0;
 
     /**
      * Copy data from an other device on the same backend.
      * @param device (backend, device) pair to copy from. The backend must match current implementation backend.
      * @param src Pointer on current implementation backend.
      * @param length Number of elements to copy.
+     * @param offset Destination offset (in number of elements).
     */
-    virtual void copyFromDevice(const void *src, NbElts_t length, const std::pair<std::string, DeviceIdx_t>& device) = 0;
+    virtual void copyFromDevice(const void *src, const std::pair<std::string, DeviceIdx_t>& device, NbElts_t length, NbElts_t offset = 0) = 0;
 
     /**
      * Copy data from host.
      * @param src Host pointer to copy from.
      * @param length Number of elements to copy.
+     * @param offset Destination offset (in number of elements).
     */
-    virtual void copyFromHost(const void *src, NbElts_t length) = 0;
+    virtual void copyFromHost(const void *src, NbElts_t length, NbElts_t offset = 0) = 0;
 
     /**
      * Copy data to host.
      * @param src Host pointer to copy to.
      * @param length Number of elements to copy.
+     * @param offset Source offset (in number of elements).
     */
-    virtual void copyToHost(void *dst, NbElts_t length) const = 0;
+    virtual void copyToHost(void *dst, NbElts_t length, NbElts_t offset = 0) const = 0;
 
     /**
      * Return the raw device pointer.
@@ -143,25 +167,43 @@ public:
     */
     virtual void setRawPtr(void* /*ptr*/, NbElts_t /*length*/)
     {
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "Cannot set raw pointer for backend %s", mBackend);
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "Cannot set raw pointer for backend {}", mBackend);
     };
 
-    virtual std::size_t size() const = 0; // Storage size
-    virtual std::size_t scalarSize() const = 0; // Size of one scalar (in bytes)
-    constexpr const char *backend() const { return mBackend; }
-    virtual ~TensorImpl() = default;
-    virtual bool operator==(const TensorImpl &othImpl) const = 0;
+    /**
+     * @brief Set the size, in number of elements, that must be stored.
+    */
+    virtual void resize(std::vector<DimSize_t> dims) {
+        mNbElts = std::accumulate(dims.cbegin(), dims.cend(), std::size_t(1), std::multiplies<std::size_t>());
+    }
+
+    /**
+     * @brief Return the number of elements stored.
+    */
+    inline std::size_t size() const noexcept { return mNbElts; }
 
     /**
-     * Copy from another backend.
+     * @brief Return the size (in bytes) of one element (scalar).
+    */
+    virtual std::size_t scalarSize() const noexcept = 0;
+
+    /**
+     * @brief Set every element of the implementation to zero.
+     */
+    virtual void zeros() {
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "Function not implented");
+    }
+
+    const std::string backend() const { return mBackend; }
+
+    /**
+     * @brief Copy from another backend.
      * @param srcImpl Source TensorImpl to copy from.
      * @param length Number of elements of size scalarSize() to copy
+     * @param srcOffset Source offset (in number of elements).
+     * @param dstOffset Destination offset (in number of elements).
     */
-    void copyFrom(const TensorImpl& srcImpl, NbElts_t length);
-
-protected:
-    const char *mBackend;
-    DeviceIdx_t mDevice;
+    void copyFrom(const TensorImpl& srcImpl, NbElts_t length, NbElts_t srcOffset = 0, NbElts_t dstOffset = 0);
 };
 
 } // namespace Aidge
diff --git a/include/aidge/backend/cpu/data/GetCPUPtr.h b/include/aidge/backend/cpu/data/GetCPUPtr.h
new file mode 100644
index 0000000000000000000000000000000000000000..b3e0fd967457585e7e3719f92dde7d6d93eee903
--- /dev/null
+++ b/include/aidge/backend/cpu/data/GetCPUPtr.h
@@ -0,0 +1,27 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CPU_DATA_GETCPUPTR_H_
+#define AIDGE_CPU_DATA_GETCPUPTR_H_
+
+#include <cstddef>
+#include <memory>
+
+#include "aidge/data/Tensor.hpp"
+
+namespace Aidge {
+inline void *getCPUPtr(std::shared_ptr<Aidge::Data> const &data, const std::size_t offset = 0) {
+  const auto tensor = std::static_pointer_cast<Tensor>(data);
+  return tensor->getImpl()->hostPtr(tensor->getImplOffset() + offset);
+}
+} // namespace Aidge
+
+#endif // AIDGE_CPU_DATA_GETCPUPTR_H_
diff --git a/include/aidge/backend/cpu/data/TensorImpl.hpp b/include/aidge/backend/cpu/data/TensorImpl.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..922acacb070c745b2924d1fb787602326ec9d05a
--- /dev/null
+++ b/include/aidge/backend/cpu/data/TensorImpl.hpp
@@ -0,0 +1,140 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CPU_DATA_TENSORIMPL_H_
+#define AIDGE_CPU_DATA_TENSORIMPL_H_
+
+#include "aidge/backend/TensorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/future_std/span.hpp"
+
+namespace Aidge {
+
+template <class T>
+class TensorImpl_cpu : public TensorImpl {
+private:
+    /// Pointer to the data and its capacity
+    future_std::span<T> mData;
+    /// If this instance own the data, std::unique_ptr manages it
+    std::unique_ptr<T[]> mDataOwner;
+
+public:
+    static const std::string Backend;
+
+public:
+    TensorImpl_cpu(DeviceIdx_t device, std::vector<DimSize_t> dims) : TensorImpl(Backend, device, dims) {}
+
+    bool operator==(const TensorImpl &other) const override final;
+
+    static std::shared_ptr<TensorImpl_cpu> create(DeviceIdx_t device, std::vector<DimSize_t> dims) {
+        return std::make_shared<TensorImpl_cpu<T>>(device, dims);
+    }
+
+    inline std::size_t scalarSize() const noexcept override final { return sizeof(T); }
+
+    void zeros() override final;
+
+    void copy(const void *src, NbElts_t length, NbElts_t offset = 0) override final {
+        const T* srcT = static_cast<const T *>(src);
+        T* dstT = static_cast<T *>(rawPtr(offset));
+
+        AIDGE_ASSERT(length <= mData.size() || length <= mNbElts, "copy length is above capacity");
+        AIDGE_ASSERT(dstT < srcT || dstT >= srcT + length, "overlapping copy is not supported");
+        std::copy(srcT, srcT + length, dstT);
+    }
+
+    void copyCast(const void *src, const DataType srcDt, NbElts_t length, NbElts_t offset = 0) override final;
+
+    void copyFromDevice(const void *src, const std::pair<std::string, DeviceIdx_t>& device, NbElts_t length, NbElts_t offset = 0) override final {
+        AIDGE_ASSERT(device.first == Backend, "backend must match");
+        AIDGE_ASSERT(device.second == 0, "device cannot be != 0 for CPU backend");
+        copy(src, length, offset);
+    }
+
+    inline void copyFromHost(const void *src, NbElts_t length, NbElts_t offset = 0) override final {
+        copy(src, length, offset);
+    }
+
+    void copyToHost(void *dst, NbElts_t length, NbElts_t offset = 0) const override final {
+        const T* src = static_cast<const T*>(rawPtr(offset));
+        AIDGE_ASSERT(length <= mData.size() || length <= mNbElts, "copy length is above capacity");
+        std::copy(src, src + length, static_cast<T *>(dst));
+    }
+
+    void *rawPtr(NbElts_t offset = 0) override final {
+        lazyInit();
+        return (mData.data() + offset);
+    };
+
+    const void *rawPtr(NbElts_t offset = 0) const override final {
+        AIDGE_ASSERT(mData.size() >= mNbElts, "accessing uninitialized const rawPtr");
+        return (mData.data() + offset);
+    };
+
+    void *hostPtr(NbElts_t offset = 0) override final {
+        lazyInit();
+        return (mData.data() + offset);
+    };
+
+    const void *hostPtr(NbElts_t offset = 0) const override final {
+        AIDGE_ASSERT(mData.size() >= mNbElts, "accessing uninitialized const hostPtr");
+        return (mData.data() + offset);
+    };
+
+    void setRawPtr(void *ptr, NbElts_t length) override final {
+        AIDGE_ASSERT(length >= mNbElts, "trying to set raw pointer of insufficient capacity");
+        mData = future_std::span<T>(static_cast<T *>(ptr), length);
+        mDataOwner.reset();
+    };
+
+    virtual ~TensorImpl_cpu() = default;
+
+private:
+    void lazyInit() {
+        if (mData.size() < mNbElts) {
+            // Need more data, a re-allocation will occur
+            AIDGE_ASSERT(mData.empty() || mDataOwner != nullptr, "trying to enlarge non-owned data");
+            mDataOwner.reset(new T[mNbElts]);
+            mData = future_std::span<T>(mDataOwner.get(), mNbElts);
+        }
+    }
+};
+
+
+template <typename T>
+const std::string TensorImpl_cpu<T>::Backend = "cpu";
+
+namespace {
+static Registrar<Tensor> registrarTensorImpl_cpu_Float64(
+        {"cpu", DataType::Float64}, Aidge::TensorImpl_cpu<double>::create);
+static Registrar<Tensor> registrarTensorImpl_cpu_Float32(
+        {"cpu", DataType::Float32}, Aidge::TensorImpl_cpu<float>::create);
+static Registrar<Tensor> registrarTensorImpl_cpu_Float16(
+        {"cpu", DataType::Float16}, Aidge::TensorImpl_cpu<half_float::half>::create);
+static Registrar<Tensor> registrarTensorImpl_cpu_Int64(
+        {"cpu", DataType::Int64}, Aidge::TensorImpl_cpu<long>::create);
+static Registrar<Tensor> registrarTensorImpl_cpu_Int32(
+        {"cpu", DataType::Int32}, Aidge::TensorImpl_cpu<int>::create);
+static Registrar<Tensor> registrarTensorImpl_cpu_Int16(
+        {"cpu", DataType::Int16}, Aidge::TensorImpl_cpu<int16_t>::create);
+static Registrar<Tensor> registrarTensorImpl_cpu_UInt16(
+        {"cpu", DataType::UInt16}, Aidge::TensorImpl_cpu<uint16_t>::create);
+static Registrar<Tensor> registrarTensorImpl_cpu_Int8(
+        {"cpu", DataType::Int8}, Aidge::TensorImpl_cpu<int8_t>::create);
+static Registrar<Tensor> registrarTensorImpl_cpu_UInt8(
+        {"cpu", DataType::UInt8}, Aidge::TensorImpl_cpu<uint8_t>::create);
+}  // namespace
+}  // namespace Aidge
+
+#endif /* AIDGE_CPU_DATA_TENSORIMPL_H_ */
diff --git a/include/aidge/data/Data.hpp b/include/aidge/data/Data.hpp
index bf34860fbc4e4d6cfef8528d20de40c3e31a292b..a6ff03d36b662f4420424f930401844de25036d2 100644
--- a/include/aidge/data/Data.hpp
+++ b/include/aidge/data/Data.hpp
@@ -47,14 +47,15 @@ enum class DataType {
 
 class Data {
 public:
-    constexpr Data(const char* type): mType(type) {};
-    constexpr const char* type() const {
+    Data(const std::string& type): mType(type) {};
+    constexpr const std::string& type() const {
         return mType;
     }
     virtual ~Data() = default;
+    virtual std::string toString() const = 0;
 
 private:
-    const char* mType;
+    const std::string mType;
 };
 }
 
@@ -80,4 +81,8 @@ const char* const EnumStrings<Aidge::DataType>::data[]
        "UInt7", "UInt8", "UInt16", "UInt32", "UInt64"};
 }
 
-#endif /* AIDGE_DATA_H_ */
\ No newline at end of file
+namespace Aidge {
+inline auto format_as(DataType dt) { return EnumStrings<Aidge::DataType>::data[static_cast<int>(dt)]; }
+}
+
+#endif /* AIDGE_DATA_H_ */
diff --git a/include/aidge/data/DataProvider.hpp b/include/aidge/data/DataProvider.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..62d10a6983e8cf5fd8e2730d3203bed97284e336
--- /dev/null
+++ b/include/aidge/data/DataProvider.hpp
@@ -0,0 +1,144 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CORE_DATA_DATAPROVIDER_H_
+#define AIDGE_CORE_DATA_DATAPROVIDER_H_
+
+#include <cstddef>  // std::size_t
+#include <memory>   // std::shared_ptr
+#include <string>
+#include <vector>   // std::vector
+
+#include "aidge/data/Database.hpp"
+#include "aidge/data/Data.hpp"
+
+namespace Aidge {
+
+/**
+ * @brief Data Provider. Takes in a database and compose batches by fetching data from the given database.
+ * @todo Implement Drop last batch option. Currently returns the last batch with less elements in the batch.
+ * @todo Implement readRandomBatch to compose batches from the database with a random sampling startegy. Necessary for training.
+ */
+class DataProvider {
+private:
+    // Dataset providing the data to the dataProvider
+    const Database& mDatabase;
+    
+    // Desired size of the produced batches
+    const std::size_t mBatchSize;
+
+    // Enable random shuffling for learning
+    const bool mShuffle;
+
+    // Drops the last non-full batch
+    const bool mDropLast;
+
+    // Number of modality in one item
+    const std::size_t mNumberModality;
+
+    // mNbItems contains the number of items in the database
+    std::size_t mNbItems;
+    // mBatches contains the call order of each database item
+    std::vector<unsigned int> mBatches; 
+    // mIndex browsing the number of batch
+    std::size_t mIndexBatch;
+
+    // mNbBatch contains the number of batch
+    std::size_t mNbBatch;
+    // Size of the Last batch
+    std::size_t mLastBatchSize;
+
+    // Store each modality dimensions, backend and type
+    std::vector<std::vector<std::size_t>> mDataDims;
+    std::vector<std::string> mDataBackends;
+    std::vector<DataType> mDataTypes; 
+
+public:
+    /**
+     * @brief Constructor of Data Provider.
+     * @param database database from which to load the data.
+     * @param batchSize number of data samples per batch.
+     */
+    DataProvider(const Database& database, const std::size_t batchSize, const bool shuffle = false, const bool dropLast = false);
+
+public:
+    /**
+     * @brief Create a batch for each data modality in the database.
+     * @return a vector of tensors. Each tensor is a batch corresponding to one modality.
+     */
+    std::vector<std::shared_ptr<Tensor>> readBatch() const;
+
+    /**
+     * @brief Get the Number of Batch.
+     * 
+     * @return std::size_t 
+     */
+    inline std::size_t getNbBatch(){
+        return mNbBatch;
+    };
+
+    /**
+     * @brief Get the current Index Batch.
+     * 
+     * @return std::size_t 
+     */
+    inline std::size_t getIndexBatch(){
+        return mIndexBatch;
+    };
+
+    /**
+     * @brief Reset the internal index batch that browses the data of the database to zero.
+     */
+    inline void resetIndexBatch(){
+        mIndexBatch = 0;
+    };
+
+    /**
+     * @brief Increment the internal index batch that browses the data of the database.
+     */
+    inline void incrementIndexBatch(){
+        ++mIndexBatch;
+    };
+
+    /**
+     * @brief Setup the batches for one pass on the database.
+     */
+    void setBatches();
+
+    /**
+     * @brief End condition of dataProvider for one pass on the database.
+     * 
+     * @return true when all batch were fetched, False otherwise
+     */
+    inline bool done(){
+        return (mIndexBatch == mNbBatch);
+    };
+
+
+    // Functions for python iterator iter and next (definition in pybind.cpp)
+    /**
+     * @brief __iter__ method for iterator protocol
+     * 
+     * @return DataProvider* 
+     */
+    DataProvider* iter();
+
+    /**
+     * @brief __next__ method for iterator protocol
+     * 
+     * @return std::vector<std::shared_ptr<Aidge::Tensor>> 
+     */
+    std::vector<std::shared_ptr<Aidge::Tensor>> next();
+};
+
+} // namespace Aidge
+
+#endif /* AIDGE_CORE_DATA_DATAPROVIDER_H_ */
diff --git a/include/aidge/data/Database.hpp b/include/aidge/data/Database.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..edd4b4639fb415dfd723aca987ae754f6d5ccc63
--- /dev/null
+++ b/include/aidge/data/Database.hpp
@@ -0,0 +1,57 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CORE_DATA_DATABASE_H_
+#define AIDGE_CORE_DATA_DATABASE_H_
+
+#include <cstddef>
+#include <memory>
+#include <vector>
+
+#include "aidge/data/Tensor.hpp"
+
+namespace Aidge {
+
+/**
+ * @brief Abstract class representing a map from a key to data.
+ * All databases should inherit from this class. All subclasses should overwrite
+ * :cpp:function:`Database::getItem` to fetch data from a given index.
+ */
+class Database {
+public:
+    Database() = default;
+    virtual ~Database() noexcept = default;
+
+    /**
+     * @brief Fetch an item of the database.
+     * @param index index of the item.
+     * @return vector of data mapped to index.
+     */
+    virtual std::vector<std::shared_ptr<Tensor>> getItem(const std::size_t index) const = 0;
+
+    /**
+     * @brief Get the number of items in the database
+     *
+     * @return std::size_t
+     */
+    virtual std::size_t getLen() const noexcept = 0;
+
+    /**
+     * @brief Get the number of modalities in one database item
+     *
+     * @return std::size_t
+     */
+    virtual std::size_t getNbModalities() const noexcept = 0;
+
+};
+} // namespace Aidge
+
+#endif /* AIDGE_CORE_DATA_DATABASE_H_ */
diff --git a/include/aidge/data/Elts.hpp b/include/aidge/data/Elts.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..1a5a9e10ea131751ff5616eb2c310068d42ce991
--- /dev/null
+++ b/include/aidge/data/Elts.hpp
@@ -0,0 +1,124 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_ELTS_H_
+#define AIDGE_ELTS_H_
+
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Types.h"
+
+namespace Aidge {
+/**
+ * Base object for Aidge consumer-producer model (C-P model).
+ * It is a hybrid model: operator implementations can specify their C-P model
+ * with precise data (bytes) or with tokens.
+*/
+struct Elts_t {
+    enum EltType {
+        Data,
+        Token,
+        Undef
+    };
+
+    NbElts_t data;
+    NbElts_t token;
+    EltType type;
+
+    // Addition operator
+    inline Elts_t operator+(const Elts_t& other) const {
+        AIDGE_ASSERT(type == other.type || other.type == Undef || type == Undef,
+            "Incompatible C-P model types: {} + {}. Data and Token cannot be mixed.", type, other.type);
+        return Elts_t(data + other.data, token + other.token, (other.type == Undef) ? type : other.type);
+    }
+
+    // Addition assignment operator
+    inline Elts_t& operator+=(const Elts_t& other) {
+        AIDGE_ASSERT(type == other.type || other.type == Undef || type == Undef,
+            "Incompatible C-P model types: {} += {}. Data and Token cannot be mixed.", type, other.type);
+        data += other.data;
+        token += other.token;
+        type = (other.type == Undef) ? type : other.type;
+        return *this;
+    }
+
+    // Comparison operators
+    inline bool operator<(const Elts_t& other) const {
+        if (type == Elts_t::Undef || type == Elts_t::Token) {
+            // Nothing, or only a token is required: don't care about how much data has been produced for the token
+            return (token < other.token);
+        }
+        else if (type == Elts_t::Data && other.type != Elts_t::Token) {
+            // A precise amount of data is required, so the amount of produced data must be specified, a token is not enough
+            return (data < other.data);
+        }
+        else {
+            AIDGE_THROW_OR_ABORT(std::runtime_error,
+                "Incompatible C-P model types: {} < {}. Data is expected for right-hand side.", type, other.type);
+        }
+    }
+
+    inline bool operator>(const Elts_t& other) const {
+        if (type == Elts_t::Undef || type == Elts_t::Token) {
+            // Nothing, or only a token is required: don't care about how much data has been produced for the token
+            return (token > other.token);
+        }
+        else if (type == Elts_t::Data && other.type != Elts_t::Token) {
+            // A precise amount of data is required, so the amount of produced data must be specified, a token is not enough
+            return (data > other.data);
+        }
+        else {
+            AIDGE_THROW_OR_ABORT(std::runtime_error,
+                "Incompatible C-P model types: {} > {}. Data is expected for right-hand side.", type, other.type);
+        }
+    }
+
+    inline static Elts_t NoneElts() {
+        return Elts_t(0, 0, Elts_t::Undef);
+    }
+
+    inline static Elts_t DataElts(NbElts_t data, NbElts_t token = 1) {
+        return Elts_t(data, token, Elts_t::Data);
+    }
+
+    inline static Elts_t TokenElts(NbElts_t token) {
+        return Elts_t(0, token, Elts_t::Token);
+    }
+
+private:
+    inline Elts_t(NbElts_t data_, NbElts_t token_, EltType type_):
+        data(data_), token(token_), type(type_) {}
+};
+} // end namespace Aidge
+
+template<>
+struct fmt::formatter<Aidge::Elts_t> {
+    template<typename ParseContext>
+    inline constexpr auto parse(ParseContext& ctx) {
+        return ctx.begin();
+    }
+
+    template<typename FormatContext>
+    inline auto format(Aidge::Elts_t const& elt, FormatContext& ctx) {
+        return fmt::format_to(ctx.out(), "{}:{}", elt.data, elt.token);
+    }
+};
+
+namespace {
+template <>
+const char* const EnumStrings<Aidge::Elts_t::EltType>::data[]
+    = {"Data", "Token", "Undef"};
+}
+
+namespace Aidge {
+inline auto format_as(Elts_t::EltType elt) { return EnumStrings<Aidge::Elts_t::EltType>::data[static_cast<int>(elt)]; }
+}
+
+#endif /* AIDGE_ELTS_H_ */
diff --git a/include/aidge/data/Tensor.hpp b/include/aidge/data/Tensor.hpp
index 8129a900718169861dc2df4213cd3533d1dfe570..b8623450a9c793e4efaff00d87455ab88aa60207 100644
--- a/include/aidge/data/Tensor.hpp
+++ b/include/aidge/data/Tensor.hpp
@@ -12,15 +12,22 @@
 #ifndef AIDGE_CORE_DATA_TENSOR_H_
 #define AIDGE_CORE_DATA_TENSOR_H_
 
+#include <cstddef>      // std::size_t
 #include <cstring>
+#include <functional>   // std::multiplies
 #include <set>
 #include <memory>
-#include <numeric>   // std::accumulate
+#include <numeric>      // std::accumulate
 #include <string>
+#include <type_traits>  // std::is_arithmetic
 #include <vector>
 
 #include "aidge/backend/TensorImpl.hpp"
 #include "aidge/data/Data.hpp"
+#include "aidge/operator/Add.hpp"
+#include "aidge/operator/Div.hpp"
+#include "aidge/operator/Mul.hpp"
+#include "aidge/operator/Sub.hpp"
 #include "aidge/utils/Registrar.hpp"
 #include "aidge/utils/Types.h"
 #include "aidge/utils/ArrayHelpers.hpp"
@@ -32,46 +39,68 @@ namespace Aidge {
  * Contains a pointer to an actual contiguous implementation of data.
  */
 class Tensor : public Data,
-               public Registrable<Tensor, std::tuple<std::string, DataType>, std::unique_ptr<TensorImpl>(const Tensor &)> {
+               public Registrable<Tensor, std::tuple<std::string, DataType>, std::shared_ptr<TensorImpl>(DeviceIdx_t device, std::vector<DimSize_t> dims)> {
    private:
-    DataType mDataType; /** enum to specify data type. */
+    DataType mDataType = DataType::Float32; /** enum to specify data type. */
     std::vector<DimSize_t> mDims; /** Dimensions of the tensor. */
-    std::unique_ptr<TensorImpl> mImpl; /** Pointer to the actual data implementation. */
-    std::shared_ptr<Tensor> mGrad; /** Pointer to the associated gradient Tensor instance. */
+    std::vector<DimSize_t> mStrides; /** Stride dimensions of the tensor. */
+    std::shared_ptr<TensorImpl> mImpl = nullptr; /** Pointer to the actual data implementation. */
+    std::size_t mImplOffset = 0;
+    std::shared_ptr<Tensor> mGrad = nullptr; /** Pointer to the associated gradient Tensor instance. */
 
     // Cached data
-    std::size_t mSize = 0;    /** Number of elements in the Tensor. */
+    /// @brief Number of elements in the Tensor.
+    std::size_t mSize;
+    /// @brief Whether or not data are contiguous in memory.
+    bool mContiguous = true;
 
    public:
     static constexpr const char *Type = "Tensor";
 
     /**
      * @brief Construct a new empty Tensor object.
-     * @param dataType Sets the type of inserted data.
+     * It has the features of an undefined scalar.
      */
-    Tensor(DataType dataType = DataType::Float32)
+    Tensor(DataType dtype = DataType::Float32)
         : Data(Type),
-          mDataType(dataType)
+          mDataType(dtype),
+          mDims(std::vector<DimSize_t>({})),
+          mStrides({1}),
+          mSize(1)
     {
         // ctor
     }
 
     /**
-     * @brief Construct a new Tensor object copied from another one.
-     * @param otherTensor
+     * @brief Construct a new Tensor object from an arithmetic parameter.
+     *
+     * @tparam T Type of the input parameter.
+     * @tparam VT Decayed type of the input paramter.
+     * @param val Input value.
      */
-    Tensor(const Tensor& otherTensor)
+    template<typename T,
+             typename VT = std::enable_if_t<std::is_arithmetic<T>::value, std::decay_t<T>>>
+    Tensor(T val)
         : Data(Type),
-          mDataType(otherTensor.mDataType),
-          mDims(otherTensor.mDims),
-          mSize(otherTensor.mSize)
+          mDataType(NativeType<VT>::type),
+          mDims({}),
+          mStrides({1}),
+          mImpl(Registrar<Tensor>::create({"cpu", NativeType<VT>::type})(0, std::vector<std::size_t>())),
+          mSize(1)
     {
-        if (otherTensor.hasImpl()) {
-            mImpl = Registrar<Tensor>::create({otherTensor.mImpl->backend(), dataType()})(*this);
-            mImpl->setDevice(otherTensor.mImpl->device().second);
-            // Same backend, same device => directly use copy()
-            mImpl->copy(otherTensor.mImpl->rawPtr(), mSize);
-        }
+        *static_cast<VT*>(mImpl->rawPtr()) = static_cast<VT>(val);
+    }
+
+    /**
+     * @brief Construct a new Tensor object from dimensions.
+     *
+     * @param dims dimensions of the tensor
+     */
+    Tensor(const std::vector<DimSize_t>& dims)
+        : Data(Type)
+    {
+        // set mDims, mStrides, mContiguous, mSize
+        resize(dims);
     }
 
     /**
@@ -84,19 +113,11 @@ class Tensor : public Data,
         : Data(Type),
           mDataType(NativeType<T>::type),
           mDims({SIZE_0}),
-          mImpl(Registrar<Tensor>::create({"cpu", NativeType<T>::type})(*this)),
-          mSize(SIZE_0) {
-        mImpl->copyFromHost(&arr.data[0], SIZE_0);
-    }
-
-    template <typename T, std::size_t SIZE_0>
-    constexpr Tensor &operator=(Array1D<T, SIZE_0> &&arr) {
-        resize({SIZE_0});
-        if (!mImpl) {
-            mImpl = Registrar<Tensor>::create({"cpu", NativeType<T>::type})(*this);
-        }
+          mStrides({1}),
+          mImpl(Registrar<Tensor>::create({"cpu", NativeType<T>::type})(0, {SIZE_0})),
+          mSize(SIZE_0)
+    {
         mImpl->copyFromHost(&arr.data[0], SIZE_0);
-        return *this;
     }
 
     /**
@@ -110,21 +131,12 @@ class Tensor : public Data,
         : Data(Type),
           mDataType(NativeType<T>::type),
           mDims({SIZE_0, SIZE_1}),
-          mImpl(Registrar<Tensor>::create({"cpu", NativeType<T>::type})(*this)),
+          mStrides({SIZE_1, 1}),
+          mImpl(Registrar<Tensor>::create({"cpu", NativeType<T>::type})(0, {SIZE_0, SIZE_1})),
           mSize(SIZE_0 * SIZE_1) {
         mImpl->copyFromHost(&arr.data[0][0], SIZE_0 * SIZE_1);
     }
 
-    template <typename T, std::size_t SIZE_0, std::size_t SIZE_1>
-    constexpr Tensor &operator=(Array2D<T, SIZE_0, SIZE_1> &&arr) {
-        resize({SIZE_0, SIZE_1});
-        if (!mImpl) {
-            mImpl = Registrar<Tensor>::create({"cpu", NativeType<T>::type})(*this);
-        }
-        mImpl->copyFromHost(&arr.data[0][0], SIZE_0 * SIZE_1);
-        return *this;
-    }
-
     /**
      * @brief Construct a new Tensor object from the 3-dimensions Array helper.
      * @tparam T datatype
@@ -137,21 +149,12 @@ class Tensor : public Data,
         : Data(Type),
           mDataType(NativeType<T>::type),
           mDims({SIZE_0, SIZE_1, SIZE_2}),
-          mImpl(Registrar<Tensor>::create({"cpu", NativeType<T>::type})(*this)),
+          mStrides({SIZE_1 * SIZE_2, SIZE_2, 1}),
+          mImpl(Registrar<Tensor>::create({"cpu", NativeType<T>::type})(0, {SIZE_0, SIZE_1, SIZE_2})),
           mSize(SIZE_0 * SIZE_1 * SIZE_2) {
         mImpl->copyFromHost(&arr.data[0][0][0], SIZE_0 * SIZE_1 * SIZE_2);
     }
 
-    template <typename T, std::size_t SIZE_0, std::size_t SIZE_1, std::size_t SIZE_2>
-    constexpr Tensor &operator=(Array3D<T, SIZE_0, SIZE_1, SIZE_2> &&arr) {
-        resize({SIZE_0, SIZE_1, SIZE_2});
-        if (!mImpl) {
-            mImpl = Registrar<Tensor>::create({"cpu", NativeType<T>::type})(*this);
-        }
-        mImpl->copyFromHost(&arr.data[0][0][0], SIZE_0 * SIZE_1 * SIZE_2);
-        return *this;
-    }
-
     /**
      * @brief Construct a new Tensor object from the 4-dimensions Array helper.
      * @tparam T datatype
@@ -165,43 +168,58 @@ class Tensor : public Data,
         : Data(Type),
           mDataType(NativeType<T>::type),
           mDims({SIZE_0, SIZE_1, SIZE_2, SIZE_3}),
-          mImpl(Registrar<Tensor>::create({"cpu", NativeType<T>::type})(*this)),
+          mStrides({SIZE_1 * SIZE_2 * SIZE_3, SIZE_2 * SIZE_3, SIZE_3, 1}),
+          mImpl(Registrar<Tensor>::create({"cpu", NativeType<T>::type})(0, {SIZE_0, SIZE_1, SIZE_2, SIZE_3})),
           mSize(SIZE_0 * SIZE_1 * SIZE_2 * SIZE_3) {
         mImpl->copyFromHost(&arr.data[0][0][0][0], SIZE_0 * SIZE_1 * SIZE_2 * SIZE_3);
     }
 
-    template <typename T, std::size_t SIZE_0, std::size_t SIZE_1, std::size_t SIZE_2, std::size_t SIZE_3>
-    constexpr Tensor &operator=(Array4D<T, SIZE_0, SIZE_1, SIZE_2, SIZE_3> &&arr) {
-        resize({SIZE_0, SIZE_1, SIZE_2, SIZE_3});
-        if (!mImpl) {
-            mImpl = Registrar<Tensor>::create({"cpu", NativeType<T>::type})(*this);
-        }
-        mImpl->copyFromHost(&arr.data[0][0][0][0], SIZE_0 * SIZE_1 * SIZE_2 * SIZE_3);
-        return *this;
-    }
+    /**
+     * @brief Copy constructor. Construct a new Tensor object from another one
+     * (shallow copy). Data memory is not copied, but shared between the new
+     * Tensor and the initial one.
+     * @param other
+     */
+    Tensor(const Tensor& other) = default;
+
+    /**
+     * @brief Move constructor.
+     * @param other
+     */
+    Tensor(Tensor&& other) = default;
 
     /**
-     * @brief Copy dimensions, datatype and data of another Tensor.
-     * @param t other Tensor object.
+     * @brief Copy dimensions, datatype and data from another Tensor.
+     * If current Tensor already has an implementation, data is copied to the
+     * existing implementation. Tensor backend/device remain untouched.
+     * If current Tensor does not have an implementation, only a shallow copy
+     * is performed and the Tensor will share data with t.
+     * @param other other Tensor object.
      * @return Tensor&
      */
-    Tensor &operator=(const Tensor &t) {
-        resize(t.dims());
-        setDataType(t.dataType());
-        if (t.hasImpl()) {
-            if (hasImpl()) {
-                copyCastFrom(t);
-            }
-            else {
-                mImpl = Registrar<Tensor>::create({t.mImpl->backend(), dataType()})(*this);
-                mImpl->setDevice(t.mImpl->device().second);
-                // Same backend, same device => directly use copy()
-                mImpl->copy(t.mImpl->rawPtr(), mSize);
-            }
-        }
-        else {
-            mImpl = nullptr;
-        }
+    Tensor &operator=(const Tensor& other);
+
+    template <typename T, std::size_t SIZE_0>
+    constexpr Tensor &operator=(Array1D<T, SIZE_0> &&arr) {
+        *this = Tensor(std::move(arr));
+        return *this;
+    }
+
+    template <typename T, std::size_t SIZE_0, std::size_t SIZE_1>
+    constexpr Tensor &operator=(Array2D<T, SIZE_0, SIZE_1> &&arr) {
+        *this = Tensor(std::move(arr));
+        return *this;
+    }
+
+    template <typename T, std::size_t SIZE_0, std::size_t SIZE_1, std::size_t SIZE_2>
+    constexpr Tensor &operator=(Array3D<T, SIZE_0, SIZE_1, SIZE_2> &&arr) {
+        *this = Tensor(std::move(arr));
+        return *this;
+    }
+
+    template <typename T, std::size_t SIZE_0, std::size_t SIZE_1, std::size_t SIZE_2, std::size_t SIZE_3>
+    constexpr Tensor &operator=(Array4D<T, SIZE_0, SIZE_1, SIZE_2, SIZE_3> &&arr) {
+        *this = Tensor(std::move(arr));
         return *this;
     }
 
@@ -217,6 +235,125 @@ class Tensor : public Data,
         return *mImpl == *(otherTensor.mImpl);
     }
 
+    /**
+     * @brief Element-wise addition operation for two ``Tensor``s.
+     * @note ``Tensor``s should be stored on the same backend.
+     * @todo If input ``Tensor``s have a different dataType, the output should
+     * have the dataType of the ``Tensor`` with the highest precision.
+     *
+     * @param other
+     * @return Tensor
+     */
+    Tensor operator+(const Tensor& other) const {
+        AIDGE_ASSERT(hasImpl() && other.hasImpl(), "At least one Tensor cannot perform any binary operation because it has no implementation.");
+        AIDGE_ASSERT(mImpl->backend() == other.mImpl->backend(), "Tensors must have the same backend");
+        AIDGE_ASSERT(dataType() == other.dataType(), "Tensors must have the same backend");
+        auto add_ = Add_Op(2);
+        add_.associateInput(0, std::make_shared<Tensor>(*this));
+        add_.associateInput(1, std::make_shared<Tensor>(other));
+        add_.computeOutputDims();
+        add_.setDataType(dataType());
+        add_.setBackend(mImpl->backend());
+        add_.forward();
+        // using add_backend = std::remove_reference_t<decltype(*Registrar<Add_Op>::create("cpu")(std::declval<const Add_Op&>()))>;
+        return add_.getOutput(0)->clone();
+    }
+
+    /**
+     * @brief Element-wise substraction operation for two ``Tensor``s.
+     * @note ``Tensor``s should be stored on the same backend.
+     * @todo If input ``Tensor``s have a different dataType, the output should
+     * have the dataType of the ``Tensor`` with the highest precision.
+     *
+     * @param other
+     * @return Tensor
+     */
+    Tensor operator-(const Tensor& other) const {
+        AIDGE_ASSERT(hasImpl() && other.hasImpl(), "At least one Tensor cannot perform any binary operation because it has no implementation.");
+        AIDGE_ASSERT(mImpl->backend() == other.mImpl->backend(), "Tensors must have the same backend");
+        AIDGE_ASSERT(dataType() == other.dataType(), "Tensors must have the same backend");
+        auto sub_ = Sub_Op();
+        sub_.associateInput(0, std::make_shared<Tensor>(*this));
+        sub_.associateInput(1, std::make_shared<Tensor>(other));
+        sub_.computeOutputDims();
+        sub_.setDataType(dataType());
+        sub_.setBackend(mImpl->backend());
+        sub_.forward();
+        // using add_backend = std::remove_reference_t<decltype(*Registrar<Add_Op>::create("cpu")(std::declval<const Add_Op&>()))>;
+        return sub_.getOutput(0)->clone();
+    }
+
+    /**
+     * @brief Element-wise multiplication operation for two ``Tensor``s.
+     * @note ``Tensor``s should be stored on the same backend.
+     * @todo If input ``Tensor``s have a different dataType, the output should
+     * have the dataType of the ``Tensor`` with the highest precision.
+     *
+     * @param other
+     * @return Tensor
+     */
+    Tensor operator*(const Tensor& other) const {
+        AIDGE_ASSERT(hasImpl() && other.hasImpl(), "At least one Tensor cannot perform any binary operation because it has no implementation.");
+        AIDGE_ASSERT(mImpl->backend() == other.mImpl->backend(), "Tensors must have the same backend");
+        AIDGE_ASSERT(dataType() == other.dataType(), "Tensors must have the same backend");
+        auto mul_ = Mul_Op();
+        mul_.associateInput(0, std::make_shared<Tensor>(*this));
+        mul_.associateInput(1, std::make_shared<Tensor>(other));
+        mul_.computeOutputDims();
+        mul_.setDataType(dataType());
+        mul_.setBackend(mImpl->backend());
+        mul_.forward();
+        // using add_backend = std::remove_reference_t<decltype(*Registrar<Add_Op>::create("cpu")(std::declval<const Add_Op&>()))>;
+        return mul_.getOutput(0)->clone();
+    }
+
+    /**
+     * @brief Element-wise division operation for two ``Tensor``s.
+     * @note ``Tensor``s should be stored on the same backend.
+     * @todo If input ``Tensor``s have a different dataType, the output should
+     * have the dataType of the ``Tensor`` with the highest precision.
+     *
+     * @param other
+     * @return Tensor
+     */
+    Tensor operator/(const Tensor& other) const {
+        AIDGE_ASSERT(hasImpl() && other.hasImpl(), "At least one Tensor cannot perform any binary operation because it has no implementation.");
+        AIDGE_ASSERT(mImpl->backend() == other.mImpl->backend(), "Tensors must have the same backend");
+        AIDGE_ASSERT(dataType() == other.dataType(), "Tensors must have the same backend");
+        auto div_ = Div_Op();
+        div_.associateInput(0, std::make_shared<Tensor>(*this));
+        div_.associateInput(1, std::make_shared<Tensor>(other));
+        div_.computeOutputDims();
+        div_.setDataType(dataType());
+        div_.setBackend(mImpl->backend());
+        div_.forward();
+        // using add_backend = std::remove_reference_t<decltype(*Registrar<Add_Op>::create("cpu")(std::declval<const Add_Op&>()))>;
+        return div_.getOutput(0)->clone();
+    }
+
+    ~Tensor() noexcept;
+
+public:
+    /**
+     * @brief Perform a deep copy of the tensor.
+    */
+    Tensor clone() const {
+        Tensor newTensor(*this);
+        if (!newTensor.isContiguous()) {
+            newTensor.makeContiguous();
+        }
+        else {
+            std::shared_ptr<TensorImpl> newImpl = Registrar<Tensor>::create({mImpl->backend(), mDataType})(mImpl->device().second, mDims);
+            newImpl->copy(mImpl->rawPtr(mImplOffset), mSize);
+            newTensor.setImpl(newImpl);
+        }
+        return newTensor;
+    }
+
+    const std::string backend() const {
+        return hasImpl() ? getImpl()->backend() : "";
+    }
+
     /**
      * @brief Set the backend of the Tensor associated implementation. If there
      * was no previous implementation set, data will be allocated, but it will
@@ -233,17 +370,15 @@ class Tensor : public Data,
             if (mImpl->device() != std::make_pair(name, device)) {
                 // Backend change: create new impl, copy from old to new and replace
                 // impl
-                std::unique_ptr<TensorImpl> newImpl = Registrar<Tensor>::create({name, mDataType})(*this);
-                newImpl->setDevice(device);
+                std::shared_ptr<TensorImpl> newImpl = Registrar<Tensor>::create({name, mDataType})(device, mDims);
                 if (copyFrom) {
-                    newImpl->copyFrom(*mImpl, size());
+                    newImpl->copyFrom(*mImpl, mImpl->size(), mImplOffset, 0);
                 }
-                mImpl = std::move(newImpl);
+                setImpl(newImpl);
             }
         }
         else {
-            mImpl = Registrar<Tensor>::create({name, mDataType})(*this);
-            mImpl->setDevice(device);
+            mImpl = Registrar<Tensor>::create({name, mDataType})(device, mDims);
         }
     }
 
@@ -251,18 +386,13 @@ class Tensor : public Data,
      * @brief Get a list of available backends.
      * @return std::set<std::string>
      */
-    static std::set<std::string> getAvailableBackends(){
-        std::set<std::string> backendsList;
-        for(std::tuple<std::string, DataType> tupleKey : Registrar<Tensor>::getKeys())
-            backendsList.insert(std::get<0>(tupleKey));
-        return backendsList;
-    }
+    static std::set<std::string> getAvailableBackends();
 
     /**
      * @brief Get the data type enum.
      * @return constexpr DataType
      */
-    constexpr DataType dataType() const { return mDataType; }
+    constexpr DataType dataType() const noexcept { return mDataType; }
 
     /**
      * @brief Set the DataType of the Tensor and converts data
@@ -273,28 +403,39 @@ class Tensor : public Data,
      */
     void setDataType(const DataType dt, bool copyCast = true) {
         if (mImpl && (dataType() != dt)) {
-            std::unique_ptr<TensorImpl> newImpl = Registrar<Tensor>::create({mImpl->backend(), dt})(*this);
+            std::shared_ptr<TensorImpl> newImpl = Registrar<Tensor>::create({mImpl->backend(), dt})(mImpl->device().second, mDims);
             if (copyCast) {
-                newImpl->copyCast(mImpl->rawPtr(), size(), mDataType);
+                newImpl->copyCast(mImpl->rawPtr(mImplOffset), mDataType, mImpl->size());
             }
-            mImpl = std::move(newImpl);
+            setImpl(newImpl);
         }
         mDataType = dt;
     }
 
     /**
      * @brief Get the Impl object
-     * @return constexpr const std::unique_ptr<TensorImpl>&
+     * @return constexpr const std::shared_ptr<TensorImpl>&
      */
-    constexpr const std::unique_ptr<TensorImpl> &getImpl() { return mImpl; }
-    constexpr const std::unique_ptr<TensorImpl> &getImpl() const { return mImpl; }
+    constexpr const std::shared_ptr<TensorImpl>& getImpl() const noexcept { return mImpl; }
+    constexpr std::size_t getImplOffset() const noexcept { return mImplOffset; }
+
+    /**
+     * @brief Set the Impl object
+     *
+     * @param impl New impl shared pointer
+     * @param implOffset Storage offset in this new impl for this Tensor
+     */
+    void setImpl(std::shared_ptr<TensorImpl> impl, std::size_t implOffset = 0) {
+        mImpl = impl;
+        mImplOffset = implOffset;
+    }
 
     /**
      * @brief Return if an implementaiton has been associated.
      * @return true
      * @return false
      */
-    bool hasImpl() const { return (mImpl) ? true : false; }
+    bool hasImpl() const noexcept { return mImpl ? true : false; }
 
     /**
      * @brief Get number of dimensions of the Tensor.
@@ -317,13 +458,25 @@ class Tensor : public Data,
      * @brief Get dimensions of the Tensor object.
      * @return constexpr const std::vector<DimSize_t>&
      */
-    constexpr const std::vector<DimSize_t> &dims() const { return mDims; }
+    constexpr inline const std::vector<DimSize_t>& dims() const noexcept { return mDims; }
+
+    /**
+     * @brief Get strides of the Tensor object.
+     * @return constexpr const std::vector<DimSize_t>&
+     */
+    constexpr inline const std::vector<DimSize_t>& strides() const noexcept { return mStrides; }
+
+    /**
+     * @brief Return true if Tensor is contiguous in memory.
+     * @return bool
+     */
+    constexpr bool isContiguous() const noexcept { return mContiguous; }
 
     /**
      * @brief Get the number of elements in the Tensor object.
      * @return constexpr std::size_t
      */
-    constexpr std::size_t size() const { return mSize; }
+    constexpr std::size_t size() const noexcept { return mSize; }
 
     /**
      * @brief Change the dimensions of the Tensor object according to the given argument.
@@ -337,7 +490,7 @@ class Tensor : public Data,
      * @param dims New dimensions
      */
     template <std::array<DimSize_t, 1>::size_type DIM> // deducing std::array size_type and declaring DIM accordingly
-    void resize(const std::array<DimSize_t, DIM> &dims) {
+    inline void resize(const std::array<DimSize_t, DIM> &dims) {
         resize(std::vector<DimSize_t>(dims.begin(), dims.end()));
     }
 
@@ -350,11 +503,9 @@ class Tensor : public Data,
      * one, all previous data is invalided. Otherwise, previous data may or may
      * not remain valid, depending on the backend implementation.
      * @param dims New dimensions
+     * @param strides Stride of the tensor (if not specified, "nested" stride is used)
      */
-    void resize(const std::vector<DimSize_t> &dims) {
-        mDims = dims;
-        computeSize();
-    }
+    void resize(const std::vector<DimSize_t> &dims, std::vector<DimSize_t> strides = std::vector<DimSize_t>());
 
     /**
      * @brief Return if the Tensor object has at leastone element.
@@ -362,149 +513,95 @@ class Tensor : public Data,
      * @return false
      */
     bool empty() const { return mDims.empty(); }
+    // bool newempty() const noexcept {
+    //     return mSize == 0;
+    // }
+
+    /**
+     * @brief Set each element of the tensor to zero.
+     */
+    void zeros() const {
+        if (mImpl) {
+            mImpl->zeros();
+        }
+    }
 
     template <typename expectedType>
     const expectedType& get(std::size_t idx) const {
         AIDGE_ASSERT(NativeType<expectedType>::type == mDataType, "wrong data type");
         AIDGE_ASSERT(idx < mSize, "idx out of range");
-        return *reinterpret_cast<expectedType *>(mImpl->hostPtr(idx));
+        return *reinterpret_cast<expectedType *>(mImpl->hostPtr(mImplOffset + idx));
     }
 
     template <typename expectedType>
     const expectedType& get(std::vector<std::size_t> coordIdx) const {
-        return get<expectedType>(getIdx(coordIdx));
+        return get<expectedType>(getStorageIdx(coordIdx));
     }
 
     template <typename expectedType>
     void set(std::size_t idx, expectedType value){
         AIDGE_ASSERT(NativeType<expectedType>::type == mDataType, "wrong data type");
         AIDGE_ASSERT(idx < mSize, "idx out of range");
-        expectedType* dataPtr = static_cast<expectedType*>(mImpl->hostPtr(idx));
+        expectedType* dataPtr = static_cast<expectedType*>(mImpl->hostPtr(mImplOffset + idx));
         *dataPtr = value;
     }
 
     template <typename expectedType>
     void set(std::vector<std::size_t> coordIdx, expectedType value){
-        set<expectedType>(getIdx(coordIdx), value);
-    }
-
-
-
-    std::string toString() const {
-        AIDGE_ASSERT(mImpl && (dims().empty() || (dims() == std::vector<DimSize_t>({0})) || (mImpl->hostPtr() != nullptr)), "tensor should have a valid host pointer");
-
-        // TODO: move lambda elsewhere?
-        auto ptrToString = [](DataType dt, void* ptr, size_t idx) {
-            switch (dt) {
-            case DataType::Float64:
-                return std::to_string(static_cast<double*>(ptr)[idx]);
-            case DataType::Float32:
-                return std::to_string(static_cast<float*>(ptr)[idx]);
-            case DataType::Float16:
-                return std::to_string(static_cast<half_float::half*>(ptr)[idx]);
-            case DataType::Int8:
-                return std::to_string(static_cast<int8_t*>(ptr)[idx]);
-            case DataType::Int16:
-                return std::to_string(static_cast<int16_t*>(ptr)[idx]);
-            case DataType::Int32:
-                return std::to_string(static_cast<int32_t*>(ptr)[idx]);
-            case DataType::Int64:
-                return std::to_string(static_cast<int64_t*>(ptr)[idx]);
-            case DataType::UInt8:
-                return std::to_string(static_cast<uint8_t*>(ptr)[idx]);
-            case DataType::UInt16:
-                return std::to_string(static_cast<uint16_t*>(ptr)[idx]);
-            case DataType::UInt32:
-                return std::to_string(static_cast<uint32_t*>(ptr)[idx]);
-            case DataType::UInt64:
-                return std::to_string(static_cast<uint64_t*>(ptr)[idx]);
-            default:
-                AIDGE_ASSERT(true, "unsupported type to convert to string");
-            }
-            return std::string("?");  // To make Clang happy
-        };
-
-        if (dims().empty()) { return ptrToString(mDataType, mImpl->hostPtr(), 0); }
-        std::string res;
-        std::size_t dim = 0;
-        std::size_t counter = 0;
-        if (nbDims()>=2) {
-            std::vector<std::size_t> dimVals(nbDims(), 0);
-            res += "{\n";
-            while (counter < mSize) {
-                std::string spaceString = std::string((dim+1)<<1,' ');
-                if (dim < nbDims()-2) {
-                    if (dimVals[dim] == 0) {
-                        res += spaceString + "{\n";
-                        ++dim;
-                    } else if (dimVals[dim] < static_cast<std::size_t>(dims()[dim])) {
-                        res += spaceString + "},\n" + spaceString + "{\n";
-                        ++dim;
-                    } else {
-                        res += spaceString + "}\n";
-                        dimVals[dim--] = 0;
-                        dimVals[dim]++;
-                    }
-                } else {
-                    for (; dimVals[dim] < static_cast<std::size_t>(dims()[dim]); ++dimVals[dim]) {
-                        res += spaceString + "{";
-                        for (DimSize_t j = 0; j < dims()[dim + 1] - 1; ++j) {
-                            res += " " + ptrToString(mDataType, mImpl->hostPtr(), counter++) + ",";
-                        }
-                        res += " " + ptrToString(mDataType, mImpl->hostPtr(), counter++) + "}";
-                        if (dimVals[dim] < static_cast<std::size_t>(dims()[dim] - 1)) {
-                            res += ",";
-                        }
-                        res += "\n";
-                    }
-                    if (dim == 0) {
-                        break;
-                    }
-                    dimVals[dim--] = 0;
-                    dimVals[dim]++;
-                }
-            }
-
-            for(int i = static_cast<int>(dim); i > 0; --i) {
-                res += std::string((dim+1)<<1,' ') + "}\n";
-            }
-        } else {
-            res += "{";
-            for (DimSize_t j = 0; j < dims()[0]; ++j) {
-                res += " " + ptrToString(mDataType, mImpl->hostPtr(), j) + ((j < dims()[0]-1) ? "," : " ");
-            }
-        }
-        res += "}";
-        return res;
+        set<expectedType>(getStorageIdx(coordIdx), value);
     }
 
-    inline void print() const { printf("%s\n", toString().c_str()); }
+    std::string toString() const override;
+
+    inline void print() const { fmt::print("{}\n", toString()); }
 
     std::shared_ptr<Tensor> grad() {
-        if (!mGrad) {
-            mGrad = std::make_shared<Tensor>(mDataType);
-            mGrad->resize(mDims);
+        // if (!mGrad && mImpl) {
+        //     mGrad = std::make_shared<Tensor>(mDims);
+        //     mGrad->setDataType(mDataType);
+        //     mGrad->setBackend(mImpl->backend());
 
-            if (mImpl) mGrad->setBackend(mImpl->backend());
-        }
+        //     // if (mImpl) mGrad->setBackend(mImpl->backend());
+        // }
 
         return mGrad;
     }
 
+    /**
+     * @brief Associate the gradient with a Tensor instance and set its implementation
+     * if none was previously set.
+     * @note Dimensions for the Tensor instance are copied from the original current Tensor.
+     * @note If a Tensor instance was already associated, only the implementation is created
+     * with values set to 0.
+     * @note If Tensor instance and implementation already existed for the gradient
+     * nothing is done.
+     */
+    void initGradient() {
+        if (!mGrad) {
+            mGrad = std::make_shared<Tensor>(mDims);
+        }
+        if (!mGrad->hasImpl()) {
+            mGrad->setDataType(dataType());
+            mGrad->setBackend(hasImpl() ? mImpl->backend() : "cpu");
+            mGrad->zeros();
+        }
+    }
+
     /**
      * @brief From the the 1D contiguous index, return the coordinate of an element in the tensor.
+     * Beware: do not use this function with the storage index!
      *
      * @param flatIdx 1D contiguous index of the value considering a flatten, contiguous, tensor.
      * @return std::vector<DimSize_t>
      */
     std::vector<std::size_t> getCoord(std::size_t flatIdx) const {
-        std::vector<std::size_t> coordIdx = std::vector<std::size_t>(mDims.size());
-        std::size_t idx = flatIdx;
-        for (std::size_t i = mDims.size() - 1; i > 0; --i){
-            coordIdx[i] = (idx % mDims[i]);
-            idx/=mDims[i];
+        std::vector<std::size_t> coordIdx(mDims.size());
+        std::size_t i = mDims.size();
+
+        while (i-- > 0) {
+            coordIdx[i] = (flatIdx % mDims[i]);
+            flatIdx/=mDims[i];
         }
-        coordIdx[0] = idx % mDims[0];
         return coordIdx;
     }
 
@@ -512,6 +609,8 @@ class Tensor : public Data,
      * @brief From the coordinate returns the 1D contiguous index of an element in the tensor.
      * If the number of coordinates is inferior to the number of dimensions,
      * the remaining coordinates are assumed to be 0.
+     * Beware: the contiguous index will only correspond to the storage index
+     * if the tensor is contiguous!
      *
      * @param coordIdx Coordinate to an element in the tensor
      * @return DimSize_t Contiguous index
@@ -520,13 +619,64 @@ class Tensor : public Data,
         AIDGE_ASSERT(coordIdx.size() <= mDims.size(), "Coordinates does not match number of dimensions");
         std::size_t flatIdx = 0;
         std::size_t i = 0;
-        for(; i < coordIdx.size() - 1; ++i){
+        for(; i < coordIdx.size() - 1; ++i) {
             AIDGE_ASSERT(coordIdx[i] < mDims[i], "Coordinates dimensions does not fit the dimensions of the tensor");
             flatIdx = (flatIdx + coordIdx[i]) * mDims[i + 1];
         }
         return flatIdx + coordIdx[i];
     }
 
+    /**
+     * @brief From the coordinate returns the 1D storage index of an element in the tensor.
+     * If the number of coordinates is inferior to the number of dimensions,
+     * the remaining coordinates are assumed to be 0.
+     *
+     * @param coordIdx Coordinate to an element in the tensor
+     * @return DimSize_t Storage index
+     */
+    std::size_t getStorageIdx(const std::vector<std::size_t>& coordIdx) const {
+        for(std::size_t i = 0; i < coordIdx.size(); ++i) {
+            AIDGE_ASSERT(coordIdx[i] < mDims[i], "Coordinates dimensions does not fit the dimensions of the tensor");
+        }
+        AIDGE_ASSERT(coordIdx.size() <= mDims.size(), "Coordinates does not match number of dimensions");
+        return std::inner_product(coordIdx.cbegin(), coordIdx.cend(), mStrides.cbegin(), DimSize_t(0));
+    }
+
+    /**
+     * @brief Returns a sub-tensor with equal or lower number of dimensions.
+     *
+     * @note For instance, ``t.extract({1})`` on a CHW tensor will return the HW tensor
+     * of channel #1.
+     * Likewise, ``t.extract({0, 1})`` on a NCHW tensor will return the HW tensor
+     * of batch #0 and channel #1.
+     * @note No memory copy is performed, the returned tensor does not own the memory.
+     * @note If the number of coordinates matches the number of dimensions, a scalar
+     * tensor is returned.
+     * @note If current tensor was contiguous, the returned tensor is garanteed to be
+     * contiguous as well.
+     *
+     * @param coordIdx Coordinates of the sub-tensor to extract
+     * @return Tensor Sub-tensor.
+    */
+    Tensor extract(const std::vector<std::size_t>& coordIdx) const;
+
+    /**
+     * @brief Returns a sub-tensor at some coordinate and with some dimension.
+     *
+     * @note Data contiguity of the returned Tensor is not guaranted.
+     *
+     * @param coordIdx First coordinates of the sub-tensor to extract
+     * @param dims Dimensions of the sub-tensor to extract
+     * @return Tensor Sub-tensor.
+    */
+    Tensor extract(const std::vector<std::size_t>& coordIdx, const std::vector<std::size_t>& dims) const;
+
+    /**
+     * @brief Make the tensor's storage contiguous, if it is not already the case.
+     * If not contiguous, a new memory space is allocated.
+    */
+    void makeContiguous();
+
     /**
      * Copy-cast data from a Tensor on the same device.
      * If current tensor backend/device is set and is different from src, an
@@ -572,6 +722,20 @@ class Tensor : public Data,
         copyCastFrom(src, movedSrc);
     }
 
+    /**
+     * Return a reference to a Tensor that is garanteed to be contiguous:
+     * - itself, if already contiguous;
+     * - the provided Tensor, overwritten with the copied data.
+     * The data type, backend and device stay the same.
+     * @param fallback A shared_ptr to Tensor ready to be overwritten if necessary.
+     * The shared_ptr does not need to be initialized. No new memory allocation
+     * will occur if fallback has already been allocated with the right
+     * type/size/device.
+     * @return Reference to either itself or to fallback.
+    */
+    Tensor& refContiguous(std::shared_ptr<Tensor>& fallback);
+    const Tensor& refContiguous(std::shared_ptr<Tensor>& fallback) const;
+
     /**
      * Return a reference to a Tensor casted to the desired data type:
      * - itself, if already at the right data type;
@@ -642,8 +806,49 @@ class Tensor : public Data,
         return refCastFrom(fallback, targetReqs.dataType(), device.first, device.second);
     }
 
+    /**
+     * @brief Return a reference to a Tensor on desired data type and backend/device:
+     * - itself, if already with the right characteristics;
+     * - the provided Tensor, overwritten with the right characteristics.
+     * @note no data is copy-casted. If it was so in a previous refCastFrom() on
+     * the same fallback, it remains valid, otherwise, data is invalid.
+     * @param fallback A shared_ptr to Tensor ready to be overwritten if necessary.
+     * The shared_ptr does not need to be initialized. No new memory allocation
+     * will occur if fallback has already been allocated with the right
+     * type/size/device.
+     * @param dt The desired data type.
+     * @param backend The desired backend.
+     * @param device The desired device.
+     * @return Reference to either itself or to fallback.
+    */
+    Tensor& ref(std::shared_ptr<Tensor>& fallback, const Aidge::DataType& dt, const std::string &backend, DeviceIdx_t device = 0);
+    const Tensor& ref(std::shared_ptr<Tensor>& fallback, const Aidge::DataType& dt, const std::string &backend, DeviceIdx_t device = 0) const;
+
+    /**
+     * @brief Return a reference to a Tensor with same characteristics
+     * (data type, backend/device) as targetReqs Tensor:
+     * - itself, if already with the right characteristics;
+     * - the provided Tensor, overwritten with the right characteristics.
+     * @note no data is copy-casted. If it was so in a previous refCastFrom() on
+     * the same fallback, it remains valid, otherwise, data is invalid.
+     * @param fallback A shared_ptr to Tensor ready to be overwritten if necessary.
+     * The shared_ptr does not need to be initialized. No new memory allocation
+     * will occur if fallback has already been allocated with the right
+     * type/size/device.
+     * @param targetReqs Tensor with the desired target characteristics.
+     * @return Reference to either itself or to fallback.
+    */
+    Tensor& ref(std::shared_ptr<Tensor>& fallback, const Tensor& targetReqs) {
+        const auto& device = targetReqs.getImpl()->device();
+        return ref(fallback, targetReqs.dataType(), device.first, device.second);
+    }
+
 private:
-    ///\bug not protected against overflow
+    /**
+     * @brief Compute the number of elements in the Tensor.
+     * @note If dimensions are not empty, they are multiplied to get the total number
+     * of elements. Else, the Tensor represents a scalar and contains a single element.
+     */
     void computeSize() {
         mSize = std::accumulate(mDims.begin(), mDims.end(), DimSize_t(1), std::multiplies<DimSize_t>());
     }
diff --git a/include/aidge/filler/Filler.hpp b/include/aidge/filler/Filler.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..fe39771b634278909f7eef20068cb941f9922ab8
--- /dev/null
+++ b/include/aidge/filler/Filler.hpp
@@ -0,0 +1,50 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CORE_FILLER_FILLER_H_
+#define AIDGE_CORE_FILLER_FILLER_H_
+
+#include <cstdint>  // std::uint32_t
+#include <memory>
+
+#include "aidge/data/Tensor.hpp"
+
+namespace Aidge {
+
+void calculateFanInFanOut(std::shared_ptr<Tensor> tensor,
+                                 std::uint32_t& fanIn, std::uint32_t& fanOut);
+
+enum class VarianceNorm { FanIn, Average, FanOut };
+
+template <typename T>
+void constantFiller(std::shared_ptr<Tensor> tensor, T constantValue);
+
+template <typename T>
+void normalFiller(std::shared_ptr<Tensor> tensor, double mean = 0.0,
+                  double stdDev = 1.0);
+
+template <typename T>
+void uniformFiller(std::shared_ptr<Tensor> tensor, T min, T max);
+
+template <typename T>
+void xavierUniformFiller(std::shared_ptr<Tensor> tensor, T scaling = 1.0,
+                         VarianceNorm varianceNorm = VarianceNorm::FanIn);
+template <typename T>
+void xavierNormalFiller(std::shared_ptr<Tensor> tensor, T scaling = 1.0,
+                        VarianceNorm varianceNorm = VarianceNorm::FanIn);
+
+template <typename T>
+void heFiller(std::shared_ptr<Tensor> tensor, VarianceNorm varianceNorm = VarianceNorm::FanIn,
+              T meanNorm = 0.0, T scaling = 1.0);
+
+}  // namespace Aidge
+
+#endif /* AIDGE_CORE_FILLER_FILLER_H_ */
diff --git a/include/aidge/graph/GraphView.hpp b/include/aidge/graph/GraphView.hpp
index 813301a144682ba3e99de31ae324ffaedcc5209f..845599fd32f9d2557784241d3d39747768638efa 100644
--- a/include/aidge/graph/GraphView.hpp
+++ b/include/aidge/graph/GraphView.hpp
@@ -44,10 +44,10 @@ private:
     /// @brief Set of nodes included in the graphview with names
     std::map<std::string, NodePtr> mNodeRegistry;
 
-    /// @brief GraphView inputs
+    /// @brief GraphView inputs IOIndex_t designates the input number
     std::vector<std::pair<NodePtr, IOIndex_t>> mInputNodes;
 
-    /// @brief GraphView outputs
+    /// @brief GraphView outputs IOIndex_t designates the input number
     std::vector<std::pair<NodePtr, IOIndex_t>> mOutputNodes;
 
 public:
@@ -62,11 +62,7 @@ public:
         return mNodes == gv.mNodes;
     }
 
-    NodePtr operator[](const std::string& name)
-    {
-        assert(mNodeRegistry.find(name) != mNodeRegistry.end() && "Could not find Node in the GraphView.");
-        return mNodeRegistry.at(name);
-    }
+    const NodePtr operator[](const std::string& nodeName) const;
 
 ///////////////////////////////////////////////////////
 //        FUNCTIONAL DESCRIPTION
@@ -82,66 +78,68 @@ public:
      * @brief Name of the node.
      * @return std::string
      */
-    std::string name() const;
+    inline std::string name() const noexcept { return mName; }
 
     /**
      * @brief Set the node name.
      * @warning Undefined behaviour when several Nodes have the same name.
      * @param name New name for the node.
      */
-    void setName(const std::string &name);
+    inline void setName(const std::string &name) { mName = name; }
 
     /**
      * @brief Save the GraphView as a Mermaid graph in a .md file at the
      * specified location.
      * @param path
      */
-    void save(std::string path, bool verbose = false, bool showProducers = true) const;
+    void save(const std::string& path, bool verbose = false, bool showProducers = true) const;
 
-    inline bool inView(NodePtr nodePtr) const {
-        return mNodes.find(nodePtr) != mNodes.end();
-    }
+    void logOutputs(const std::string& dirName) const;
 
-    NodePtr getRootNode() {
+    /**
+     * Check that a node is in the current GraphView.
+     * @param nodePtr Node to check
+     * @return bool True is nodePtr belongs to the GraphView.
+    */
+    bool inView(const NodePtr& nodePtr) const;
+
+    inline NodePtr rootNode() const noexcept {
         return mRootNode;
     }
 
+    void setRootNode(NodePtr node);
+
 ///////////////////////////////////////////////////////
 //        TENSOR MANAGEMENT
 ///////////////////////////////////////////////////////
 public:
     /** @brief Get reference to the set of input Nodes. */
-    inline std::set<NodePtr> inputNodes() const noexcept {
-        std::set<NodePtr> nodes;
-        for (auto node : mInputNodes) {
-            nodes.insert(node.first);
-        }
-        return nodes;
-    }
+    std::set<NodePtr> inputNodes() const;
+
     /** @brief Get reference to the set of output Nodes. */
-    inline std::set<NodePtr> outputNodes() const noexcept {
-        std::set<NodePtr> nodes;
-        for (auto node : mOutputNodes) {
-            nodes.insert(node.first);
-        }
-        return nodes;
-    }
+    std::set<NodePtr> outputNodes() const;
+
     /** @brief Assess if the given Node is an input Node of the GraphView object. */
-    inline bool isInputNode(NodePtr nodePtr) const {
-        const auto nodes = inputNodes();
-        return (nodes.find(nodePtr) != nodes.end()) ? true : false;
-    }
+    bool isInputNode(const NodePtr& nodePtr) const;
+
     /** @brief Assess if the given Node is an output Node of the GraphView object. */
-    inline bool isOutputNode(NodePtr nodePtr) const {
-        const auto nodes = outputNodes();
-        return (nodes.find(nodePtr) != nodes.end()) ? true : false;
-    }
+    bool isOutputNode(const NodePtr& nodePtr) const;
 
     void setOrderedInputs(const std::vector<std::pair<NodePtr, IOIndex_t>>& inputs);
     void setOrderedOutputs(const std::vector<std::pair<NodePtr, IOIndex_t>>& outputs);
 
-    inline const std::vector<std::pair<NodePtr, IOIndex_t>>& getOrderedInputs() { return mInputNodes; };
-    inline const std::vector<std::pair<NodePtr, IOIndex_t>>& getOrderedOutputs() { return mOutputNodes; };
+    /**
+     * @brief Get inputs of the current GraphView with their associated id.
+     * The rank of the nodes are their rank in the vector.
+     * @return const std::vector<std::pair<NodePtr, IOIndex_t>>&
+     */
+    inline const std::vector<std::pair<NodePtr, IOIndex_t>>& getOrderedInputs() const noexcept { return mInputNodes; };
+    /**
+     * @brief Get outputs of the current GraphView with their associated id.
+     * The rank of the nodes are their rank in the vector.
+     * @return const std::vector<std::pair<NodePtr, IOIndex_t>>&
+     */
+    inline const std::vector<std::pair<NodePtr, IOIndex_t>>& getOrderedOutputs() const noexcept { return mOutputNodes; };
 
     /**
      * @brief List outside data input connections of the GraphView.
@@ -172,7 +170,7 @@ public:
      * @brief List all input connections (within and outside) of the specified GraphView node named "name".
      * @return std::vector<std::pair<NodePtr, IOIndex_t>>
      */
-    std::vector<std::pair<NodePtr, IOIndex_t>> inputs(std::string name) const;
+    std::vector<std::pair<NodePtr, IOIndex_t>> inputs(const std::string& name) const;
 
     /**
      * @brief List outside output connections of the GraphView. The vector
@@ -188,7 +186,7 @@ public:
      * @return std::vector<std::vector<std::pair<NodePtr, IOIndex_t>>>
      */
     std::vector<std::vector<std::pair<NodePtr, IOIndex_t>>> outputs(
-            std::string nodeName) const;
+            const std::string& nodeName) const;
 
     /**
      * @brief Assert Datatype, Backend, data format and dimensions along the GraphView are coherent.
@@ -203,18 +201,21 @@ public:
      * If not, add a Transpose Operator.
      * 4 - Propagate Tensor dimensions through the consecutive Operators.
      */
-    void compile(const std::string& backend, const Aidge::DataType datatype, DeviceIdx_t device = 0);
+    void compile(const std::string& backend = "cpu",
+                 const Aidge::DataType datatype = DataType::Float32,
+                 DeviceIdx_t device = 0,
+                 const std::vector<std::vector<DimSize_t>> dims = {});
 
     /**
      * @brief Compute dimensions of input/output Tensors for each Operator of the
      * GraphView object's Nodes.
      */
-    void forwardDims();
+    void forwardDims(const std::vector<std::vector<DimSize_t>> dims = {});
 
     /** @brief Set the same backend for each Operator of the GraphView object's Nodes. */
-    void setBackend(const std::string &backend, DeviceIdx_t device = 0);
+    void setBackend(const std::string& backend, const DeviceIdx_t device = 0) const;
     /** @brief Set the same backend for each Operator of the GraphView object's Nodes. */
-    void setDataType(const DataType &datatype);
+    void setDataType(const DataType& datatype) const;
 
 ///////////////////////////////////////////////////////
 //        TOPOLOGY
@@ -262,6 +263,34 @@ public:
      */
     NodePtr getNode(const std::string& nodeName) const;
 
+    /**
+     * Get the ranked list of nodes in the GraphView.
+     * Node ranking if performed the following:
+     * - The root node is put in the ranked list first (rank 1);
+     * - Then, its childs (in order of outputs) are added in the ranked list;
+     * - Then, its parents (in order of inputs) are added in the ranked list;
+     * - The childs and parents of the next node in the ranked list are then
+     *   added to the list, and so on.
+     * - Any remaining nodes have no path to the root node and are added in
+     *   arbitrary order. In this case, the ranking is not garanteed to be unique.
+     *
+     * If the ranking cannot be garanteed to be unique, the second item indicates
+     * the rank from which unicity cannot be garanteed.
+     * @return std::pair<std::vector<NodePtr>, size_t> Pair with the list of ranked
+     * nodes and the size of the ranked sub-list where unicity is garanteed.
+    */
+    std::pair<std::vector<NodePtr>, size_t> getRankedNodes() const;
+
+    /**
+     * Get the nodes name according to the GraphView nodes ranking.
+     * @param format The formatting string to be used with fmt::format().
+     * The usable positional arguments are the following:
+     * {0} node name, {1} node type, {2} rank, {3} type rank
+     * @param markNonUnicity If true, non unique ranking is prefixed with "?"
+     * @return std::map<NodePtr, std::string> A map with the corresponding names
+    */
+    std::map<NodePtr, std::string> getRankedNodesName(const std::string& format, bool markNonUnicity = true) const;
+
     /**
      * @brief Remove a Node from the current GraphView scope without affecting its connections.
      * @param nodePtr Node to remove
@@ -340,11 +369,10 @@ public:
      * @param toTensor Input Tensor ID of the new Node. Default to gk_IODefaultIndex, meaning
      * first available data input for the Node.
      */
-    inline void addChild(NodePtr toOtherNode, std::string fromOutNodeName,
+    inline void addChild(NodePtr toOtherNode, const std::string& fromOutNodeName,
                          const IOIndex_t fromTensor = IOIndex_t(0),
                          IOIndex_t toTensor = gk_IODefaultIndex) {
-        assert(mNodeRegistry.find(fromOutNodeName) != mNodeRegistry.end() &&
-               "No Node with this name found in the GraphView.");
+        AIDGE_ASSERT(mNodeRegistry.find(fromOutNodeName) != mNodeRegistry.end(), "No node named {} in graph {}.", fromOutNodeName, name());
         addChild(toOtherNode, mNodeRegistry.at(fromOutNodeName), fromTensor, toTensor);
     }
 
@@ -375,7 +403,7 @@ public:
      */
     bool swap(Node &node, Node &otherNode);
 
-    void link(std::string name1_inID, std::string name2_outID);
+    void link(const std::string& name1_inID, const std::string& name2_outID);
 
     /**
      * @brief Insert a node (newParentNode) as a parent of the passed node (childNode).
@@ -411,6 +439,7 @@ public:
      * @return true replacement has been performed
      * @return false no replacement has been performed
      */
+    static bool replace(const std::shared_ptr<GraphView>& oldG, const std::shared_ptr<GraphView>& newG);
     static bool replace(const std::set<NodePtr>& oldNodes, const std::set<NodePtr>& newNodes);
 
     /**
@@ -484,8 +513,14 @@ private:
     //        TOPOLOGY
     ///////////////////////////////////////////////////////
 
-    void _forwardDims(std::set<NodePtr> listNodes);
 };
+
+/**
+ * Create a GraphView containing all nodes with a path to given argument.
+ * @param node Initial node to construct the graph.
+ * @return GraphView GraphView containing all nodes with a path to node.
+*/
+std::shared_ptr<GraphView> getConnectedGraphView(std::shared_ptr<Node> node);
 }  // namespace Aidge
 
 #endif /* AIDGE_CORE_GRAPH_GRAPHVIEW_H_ */
diff --git a/include/aidge/graph/Node.hpp b/include/aidge/graph/Node.hpp
index de2a7b6aae5357d9a1304ec2b718a475abc1ea43..908f56295887bd2fbed3350a026045a4ab6b21d9 100644
--- a/include/aidge/graph/Node.hpp
+++ b/include/aidge/graph/Node.hpp
@@ -456,7 +456,17 @@ private:
    * @param inId index for adding the parent.
    */
   void addParent(const NodePtr otherNode, const IOIndex_t inId);
+
+  // OPERATOR FUNCTIONNAL but commented out to avoid iostream inclusion
+  // /**
+  //  * @brief operator<< overload to ease print & debug of nodes
+  //  * @param[inout] ostream to print to 
+  //  * @param[in] n node to print
+  //  */
+  // friend std::ostream& operator << (std::ostream& os, Node& n); 
 };
+
 } // namespace Aidge
 
+
 #endif /* AIDGE_CORE_GRAPH_NODE_H_ */
diff --git a/include/aidge/operator/Add.hpp b/include/aidge/operator/Add.hpp
index 9aed8299a67ab719141b6fe199ebf3f52fb7d387..93cfb44514e39a489ccb75d86fd6e114da5c6162 100644
--- a/include/aidge/operator/Add.hpp
+++ b/include/aidge/operator/Add.hpp
@@ -12,15 +12,11 @@
 #ifndef AIDGE_CORE_OPERATOR_ADD_H_
 #define AIDGE_CORE_OPERATOR_ADD_H_
 
-#include <numeric>
-#include <vector>
-#include <cmath>
 #include <memory>
+#include <string>
 #include <vector>
 
-#include "aidge/utils/Registrar.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
-#include "aidge/data/Tensor.hpp"
 #include "aidge/graph/Node.hpp"
 #include "aidge/utils/Types.h"
 #include "aidge/utils/ErrorHandling.hpp"
@@ -28,7 +24,7 @@
 namespace Aidge {
 
 class Add_Op : public OperatorTensor,
-    public Registrable<Add_Op, std::string, std::unique_ptr<OperatorImpl>(const Add_Op&)> {
+    public Registrable<Add_Op, std::string, std::shared_ptr<OperatorImpl>(const Add_Op&)> {
 public:
     static const std::string Type;
 
@@ -44,11 +40,7 @@ public:
      * @brief Copy-constructor. Copy the operator attributes and its output tensor(s), but not its input tensors (the new operator has no input associated).
      * @param op Operator to copy.
      */
-    Add_Op(const Add_Op& op)
-        : OperatorTensor(op)
-    {
-        mImpl = op.mImpl ? Registrar<Add_Op>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
-    }
+    Add_Op(const Add_Op& op);
 
     /**
      * @brief Clone the operator using its copy-constructor.
@@ -68,18 +60,9 @@ public:
     // }
 
 
-    // void checkDims() const override final {
-    //     assert(outputDimsForwarded());
-    //     for (const auto& in : mInputs) {
-    //         assert(in->dims() == mOutputs[0]->dims());
-    //     }
-    // }
-
+    void computeOutputDims() override final;
 
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Add_Op>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override;
 
     static const std::vector<std::string> getInputsName() {
         return {"data_input_0", "data_input_n"};
diff --git a/include/aidge/operator/AvgPooling.hpp b/include/aidge/operator/AvgPooling.hpp
index a2098ff36b40b78eb12a36fe28793e8dd73d9d9c..4a8ca19a58427f207f9a4cae0dc9d0c29b54d7e7 100644
--- a/include/aidge/operator/AvgPooling.hpp
+++ b/include/aidge/operator/AvgPooling.hpp
@@ -13,14 +13,12 @@
 #define AIDGE_CORE_OPERATOR_AVGPOOLING_H_
 
 #include <array>
-#include <numeric>
+#include <string>
 #include <vector>
-#include <cmath>
 
-#include "aidge/data/Tensor.hpp"
 #include "aidge/graph/Node.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
-#include "aidge/operator/Producer.hpp"
+#include "aidge/utils/ArrayHelpers.hpp"
 #include "aidge/utils/StaticAttributes.hpp"
 #include "aidge/utils/Registrar.hpp"
 #include "aidge/utils/Types.h"
@@ -30,7 +28,7 @@ enum class AvgPoolingAttr { StrideDims, KernelDims };
 
 template <DimIdx_t DIM>
 class AvgPooling_Op : public OperatorTensor,
-                public Registrable<AvgPooling_Op<DIM>, std::string, std::unique_ptr<OperatorImpl>(const AvgPooling_Op<DIM> &)>,
+                public Registrable<AvgPooling_Op<DIM>, std::string, std::shared_ptr<OperatorImpl>(const AvgPooling_Op<DIM> &)>,
                 public StaticAttributes<AvgPoolingAttr,
                                        std::array<DimSize_t, DIM>,
                                        std::array<DimSize_t, DIM>> {
@@ -56,102 +54,36 @@ public:
      * @brief Copy-constructor. Copy the operator attributes and its output tensor(s), but not its input tensors (the new operator has no input associated).
      * @param op Operator to copy.
      */
-    AvgPooling_Op(const AvgPooling_Op<DIM>& op)
-        : OperatorTensor(op),
-          Attributes_(op)
-    {
-        mImpl = op.mImpl ? Registrar<AvgPooling_Op<DIM>>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
-    }
+    AvgPooling_Op(const AvgPooling_Op<DIM>& op);
 
     /**
      * @brief Clone the operator using its copy-constructor.
      * @see Operator::AvgPooling_Op
      */
-    std::shared_ptr<Operator> clone() const override {
+    std::shared_ptr<Operator> clone() const override final {
         return std::make_shared<AvgPooling_Op<DIM>>(*this);
     }
 
 
-    void computeOutputDims() override final {
-        // check inputs have been associated
-        if (!getInput(0)) {
-            AIDGE_THROW_OR_ABORT(std::runtime_error, "Every input should be associated with a Tensor");
-        }
-        if (!(getInput(0)->empty())) {
-            std::array<DimSize_t, DIM + 2> outputDims;
-            const std::array<DimSize_t, DIM + 2> inputDims(getInput(0)->template dims<DIM+2>());
-            outputDims[0] = inputDims[0];
-            outputDims[1] = inputDims[1];
-
-            for (std::size_t dim = 0; dim < this->template getAttr<AvgPoolingAttr::KernelDims>().size() ; ++dim) {
-                outputDims[dim+2] = 1 + static_cast<DimSize_t>(
-                                            std::floor(static_cast<float>(inputDims[dim+2] -
-                                                                    this->template getAttr<AvgPoolingAttr::KernelDims>()[dim]) /
-                                            static_cast<float>(this->template getAttr<AvgPoolingAttr::StrideDims>()[dim])));
-            }
-            getOutput(0)->resize(outputDims);
-        }
-    }
+    void computeOutputDims() override final;
 
 
-    std::vector<std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>>
+    std::vector<std::pair<std::vector<DimSize_t>, std::vector<DimSize_t>>>
     computeReceptiveField(const std::vector<DimSize_t>& firstEltDims,
                             const std::vector<DimSize_t>& outputDims,
-                            const IOIndex_t outputIdx = 0) const override final
-    {
-        if (outputIdx != 0) {
-            AIDGE_THROW_OR_ABORT(std::runtime_error, "Conv_Op Operator has got only one output Tensor.");
-        }
-        if (firstEltDims.size() != outputDims.size()) {
-            AIDGE_THROW_OR_ABORT(std::runtime_error, "outputDims and firstEltDims should have the size of the output Tensor dimensions.");
-        }
-        if ((outputDims.size() == (DIM+2)) && outputDimsForwarded()) {
-            // Offset
-            std::vector<DimSize_t> inputIdxDims = firstEltDims;
-
-            for (DimIdx_t i = 0; i < (DIM+2); ++i) {
-                if (((outputDims[i] + firstEltDims[i]) > mOutputs[0]->template dims<DIM+2>()[i]) || (outputDims[i] == 0)) {
-                    AIDGE_THROW_OR_ABORT(std::runtime_error, "Given outputDim out of range for dimension %lu (%lu + %lu)", static_cast<std::size_t>(i), firstEltDims[i], outputDims[i]);
-                }
-            }
-
-            // padding is not a parameter of Conv_Op. It is handled in Pad_Op Operator
-            // Width
-            std::vector<DimSize_t> inputDims;
-            inputDims.push_back(outputDims[0]); // same batch value
-            inputDims.push_back(outputDims[1]); // same channel value
-
-            for (DimIdx_t i = 0; i < DIM; ++i) {
-                inputDims.push_back((outputDims[2+static_cast<std::size_t>(i)] - 1)
-                            * this->template getAttr<AvgPoolingAttr::StrideDims>()[static_cast<std::size_t>(i)]
-                            + 1
-                            + (this->template getAttr<AvgPoolingAttr::KernelDims>()[static_cast<std::size_t>(i)] - 1));
-                inputIdxDims[2+i] *= this->template getAttr<AvgPoolingAttr::StrideDims>()[static_cast<std::size_t>(i)];
-            }
-            std::vector<std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>> res;
-            res.push_back(std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>(inputIdxDims, inputDims));
-            return res;
-        }
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "Given outputDim out of range or output dim not forwarded yet.");
-    }
+                            const IOIndex_t outputIdx = 0) const override final;
 
 
-    void setBackend(const std::string &name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<AvgPooling_Op<DIM>>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string &name, DeviceIdx_t device = 0) override final;
 
-    static const std::vector<std::string> getInputsName(){
+    static const std::vector<std::string> getInputsName() {
         return {"data_input"};
     }
-    static const std::vector<std::string> getOutputsName(){
+    static const std::vector<std::string> getOutputsName() {
         return {"data_output"};
     }
 };
 
-template <DimIdx_t DIM>
-const std::string AvgPooling_Op<DIM>::Type = "AvgPooling";
-
 template <std::array<DimSize_t, 1>::size_type DIM>
 inline std::shared_ptr<Node> AvgPooling(const std::array<DimSize_t, DIM> &kernel_dims,
                                            const std::string& name = "",
@@ -169,6 +101,12 @@ inline std::shared_ptr<Node> AvgPooling(
     static_assert(DIM<=MaxDim,"Too many kernel dimensions required by AvgPooling, not supported");
     return AvgPooling(to_array(kernel_dims), name, stride_dims);
 }
+
+extern template class Aidge::AvgPooling_Op<1>;
+extern template class Aidge::AvgPooling_Op<2>;
+extern template class Aidge::AvgPooling_Op<3>;
+extern template class Aidge::AvgPooling_Op<4>;
+
 }  // namespace Aidge
 
 namespace {
@@ -177,4 +115,4 @@ const char *const EnumStrings<Aidge::AvgPoolingAttr>::data[] = {"StrideDims",
                                                           "KernelDims"};
 }
 
-#endif /* AIDGE_CORE_OPERATOR_AVGPOOLING_H_ */
\ No newline at end of file
+#endif /* AIDGE_CORE_OPERATOR_AVGPOOLING_H_ */
diff --git a/include/aidge/operator/BatchNorm.hpp b/include/aidge/operator/BatchNorm.hpp
index 4a0f40c034c7738a33eb8a9569fac4aa2fff465d..64ae368f377d264378036e62175dc10b17aff0f4 100644
--- a/include/aidge/operator/BatchNorm.hpp
+++ b/include/aidge/operator/BatchNorm.hpp
@@ -16,13 +16,11 @@
 #include <memory>
 #include <vector>
 
-#include "aidge/utils/Types.h"
-#include "aidge/data/Tensor.hpp"
 #include "aidge/graph/Node.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
-#include "aidge/operator/Producer.hpp"
-#include "aidge/utils/StaticAttributes.hpp"
 #include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/StaticAttributes.hpp"
+#include "aidge/utils/Types.h"
 
 namespace Aidge {
 
@@ -30,7 +28,7 @@ enum class BatchNormAttr { Epsilon, Momentum };
 
 template <DimIdx_t DIM>
 class BatchNorm_Op : public OperatorTensor,
-                public Registrable<BatchNorm_Op<DIM>, std::string, std::unique_ptr<OperatorImpl>(const BatchNorm_Op<DIM> &)>,
+                public Registrable<BatchNorm_Op<DIM>, std::string, std::shared_ptr<OperatorImpl>(const BatchNorm_Op<DIM> &)>,
                 public StaticAttributes<BatchNormAttr, float, float> {
 public:
     static const std::string Type;
@@ -50,12 +48,7 @@ public:
      * @brief Copy-constructor. Copy the operator attributes and its output tensor(s), but not its input tensors (the new operator has no input associated).
      * @param op Operator to copy.
      */
-    BatchNorm_Op(const BatchNorm_Op<DIM>& op)
-        : OperatorTensor(op),
-          Attributes_(op)
-    {
-        mImpl = op.mImpl ? Registrar<BatchNorm_Op<DIM>>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
-    }
+    BatchNorm_Op(const BatchNorm_Op<DIM>& op);
 
     /**
      * @brief Clone the operator using its copy-constructor.
@@ -75,35 +68,9 @@ public:
     // }
 
 
-    void computeOutputDims() override final {
-        // check inputs have been associated
-        bool associated = true;
-        for (IOIndex_t i = 0; i < nbInputs(); ++i) {
-            associated &= !(getInput(i)->empty());
-        }
-        if (associated) {
-            const DimSize_t nbFeatures =  getInput(0)->dims()[1];
-            for (std::size_t i = nbData(); i < nbInputs(); ++i) {
-                if(getInput(i)->size() != nbFeatures) {
-                    // /!\ Input size should be handled BEFORE calling this function
-                    // This should raise an error
-                    getInput(i)->resize({getInput(0)->dims()[1]});
-                }
-            }
-            mOutputs[0]->resize(getInput(0)->dims());
-        }
-    }
+    void computeOutputDims() override final;
 
-    void setBackend(const std::string &name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<BatchNorm_Op<DIM>>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-
-        // By default, automatically set backend for scale, shift, mean and variance
-        getInput(1)->setBackend(name, device);
-        getInput(2)->setBackend(name, device);
-        getInput(3)->setBackend(name, device);
-        getInput(4)->setBackend(name, device);
-    }
+    void setBackend(const std::string &name, DeviceIdx_t device = 0) override final;
 
     static const std::vector<std::string> getInputsName() {
         return {"data_input", "scale", "shift", "mean", "variance"};
@@ -113,22 +80,19 @@ public:
     }
 };
 
-template <DimIdx_t DIM>
-const std::string BatchNorm_Op<DIM>::Type = "BatchNorm";
+extern template class Aidge::BatchNorm_Op<2>;
+extern template class Aidge::BatchNorm_Op<3>;
+extern template class Aidge::BatchNorm_Op<4>;
 
 template <DimSize_t DIM>
-inline std::shared_ptr<Node> BatchNorm(const DimSize_t nbFeatures,
+std::shared_ptr<Node> BatchNorm(const DimSize_t nbFeatures,
                                        const float epsilon = 1.0e-5F,
                                        const float momentum = 0.1F,
-                                       const std::string& name = "") {
-    static_assert(DIM<=MaxDim,"Too many kernel dimensions required by BatchNorm, not supported");
-    auto batchNorm = std::make_shared<Node>(std::make_shared<BatchNorm_Op<static_cast<DimIdx_t>(DIM)>>(epsilon, momentum), name);
-    addProducer(batchNorm, 1, {nbFeatures}, "scale");
-    addProducer(batchNorm, 2, {nbFeatures}, "shift");
-    addProducer(batchNorm, 3, {nbFeatures}, "batch_mean");
-    addProducer(batchNorm, 4, {nbFeatures}, "batch_variance");
-    return batchNorm;
-}
+                                       const std::string& name = "");
+
+extern template std::shared_ptr<Aidge::Node> Aidge::BatchNorm<2>(const DimSize_t, const float, const float, const std::string&);
+extern template std::shared_ptr<Aidge::Node> Aidge::BatchNorm<3>(const DimSize_t, const float, const float, const std::string&);
+extern template std::shared_ptr<Aidge::Node> Aidge::BatchNorm<4>(const DimSize_t, const float, const float, const std::string&);
 }  // namespace Aidge
 
 namespace {
@@ -136,4 +100,4 @@ template <>
 const char *const EnumStrings<Aidge::BatchNormAttr>::data[] = { "Epsilon", "Momentum" };
 }
 
-#endif //AIDGE_CORE_OPERATOR_BATCHNORM_H_
\ No newline at end of file
+#endif //AIDGE_CORE_OPERATOR_BATCHNORM_H_
diff --git a/include/aidge/operator/Cast.hpp b/include/aidge/operator/Cast.hpp
index 7cc3985674219daf087381049d3a845299b3e250..bbc776a1175a1fc29d08c3872649a6b7aac2f04f 100644
--- a/include/aidge/operator/Cast.hpp
+++ b/include/aidge/operator/Cast.hpp
@@ -39,7 +39,11 @@ public:
     Cast_Op(const Cast_Op& op)
         : OperatorTensor(op)
     {
-        mImpl = op.mImpl ? Registrar<Cast_Op>::create(mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl) {
+            SET_IMPL_MACRO(Cast_Op, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -50,12 +54,7 @@ public:
         return std::make_shared<Cast_Op>(*this);
     }
 
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        if (Registrar<Cast_Op>::exists({name})) {
-            mImpl = Registrar<Cast_Op>::create({name})(*this);
-        }
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override;
 
     void forward() override;
 
diff --git a/include/aidge/operator/Concat.hpp b/include/aidge/operator/Concat.hpp
index 06cc468bd7266bbcfeb6802f274c536ec09867fc..611ff6bd53b1f16f87f73dd951d0645b9765262e 100644
--- a/include/aidge/operator/Concat.hpp
+++ b/include/aidge/operator/Concat.hpp
@@ -12,16 +12,16 @@
 #ifndef AIDGE_CORE_OPERATOR_CONCAT_H_
 #define AIDGE_CORE_OPERATOR_CONCAT_H_
 
-#include <numeric>
-#include <vector>
-#include <cmath>
 #include <memory>
+#include <stdexcept>
+#include <string>
 #include <vector>
 
 #include "aidge/utils/Registrar.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
-#include "aidge/data/Tensor.hpp"
 #include "aidge/graph/Node.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Registrar.hpp"
 #include "aidge/utils/StaticAttributes.hpp"
 #include "aidge/utils/Types.h"
 
@@ -29,7 +29,7 @@ namespace Aidge {
 enum class ConcatAttr { Axis };
 
 class Concat_Op : public OperatorTensor,
-    public Registrable<Concat_Op, std::string, std::unique_ptr<OperatorImpl>(const Concat_Op&)>,
+    public Registrable<Concat_Op, std::string, std::shared_ptr<OperatorImpl>(const Concat_Op&)>,
     public StaticAttributes<ConcatAttr, DimSize_t> {
 public:
     static const std::string Type;
@@ -55,7 +55,11 @@ public:
         : OperatorTensor(op),
           Attributes_(op)
     {
-        mImpl = op.mImpl ? Registrar<Concat_Op>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl){
+            SET_IMPL_MACRO(Concat_Op, *this, op.backend());
+        }else{
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -66,45 +70,9 @@ public:
         return std::make_shared<Concat_Op>(*this);
     }
 
-    // Data operator[](const char* inputName) override final {
-    //     std::shared_ptr<Tensor> in = (strcmp(inputName, "data")) ? mInputs[0] :
-    //         (strcmp(inputName, "weight") ? mInputs[1] :
-    //         (strcmp(inputName, "bias") ? mInputs[2] :
-    //         nullptr));
-    //     assert((in!=nullptr) && "No such parameter");
-    //     return *in;
-    // }
+    void computeOutputDims() override final;
 
-
-    void computeOutputDims() override final {
-        // Every input is non-empty with the same number of dimensions
-        bool associated = (getInput(0) != nullptr);
-        associated &= !(getInput(0)->empty()) && (getAttr<ConcatAttr::Axis>() < getInput(0)->nbDims()); // do not compute anything if no input
-        auto outputDims =  getInput(0)->dims();
-        const auto firstInputNbDims = getInput(0) -> nbDims();
-        for (IOIndex_t i = 1; i < nbInputs(); ++i) {
-            if (!getInput(i)) {
-                AIDGE_THROW_OR_ABORT(std::runtime_error, "Every input should be associated with a Tensor");
-            }
-            associated &= (getInput(i)->nbDims() == firstInputNbDims);
-            for (DimSize_t dim = 0; dim < firstInputNbDims; ++dim) {
-                if (dim == getAttr<ConcatAttr::Axis>()) {
-                    outputDims[dim] += getInput(i)->dims()[dim];
-                }
-                else {
-                    associated &= (getInput(i)->dims()[dim] == outputDims[dim]);
-                }
-            }
-        }
-        if (associated) {
-            getOutput(0)->resize(outputDims);
-        }
-    }
-
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Concat_Op>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override;
 
     static const std::vector<std::string> getInputsName(){
         return {"data_input_0", "data_input_n"};
diff --git a/include/aidge/operator/Conv.hpp b/include/aidge/operator/Conv.hpp
index be5fb3e393ced7ee7a53e27426b4247e48b478e8..c93a098106be76f30c1150ea64c464492429feb9 100644
--- a/include/aidge/operator/Conv.hpp
+++ b/include/aidge/operator/Conv.hpp
@@ -13,35 +13,48 @@
 #define AIDGE_CORE_OPERATOR_CONV_H_
 
 #include <array>
-#include <cmath>
-#include <cstddef>
-#include <numeric>
+#include <cmath>    // std::floor
+#include <cstddef>  // std::size_t
+#include <string>
+#include <utility>  // std::pair
 #include <vector>
 
 #include "aidge/data/Tensor.hpp"
 #include "aidge/graph/Node.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/operator/Producer.hpp"
+#include "aidge/utils/ArrayHelpers.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Registrar.hpp" // SET_IMPL_MACRO
 #include "aidge/utils/StaticAttributes.hpp"
-#include "aidge/utils/Registrar.hpp"
 #include "aidge/utils/Types.h"
 
 namespace Aidge {
-enum class ConvAttr { StrideDims, DilationDims, InChannels, OutChannels, KernelDims };
+enum class ConvAttr { StrideDims, DilationDims, InChannels, OutChannels, KernelDims, NoBias };
 
 template <DimIdx_t DIM>
 class Conv_Op : public OperatorTensor,
-                public Registrable<Conv_Op<DIM>, std::string, std::unique_ptr<OperatorImpl>(const Conv_Op<DIM> &)>,
-                public StaticAttributes<ConvAttr, std::array<DimSize_t, DIM>, std::array<DimSize_t, DIM>, DimSize_t,
-                                       DimSize_t, std::array<DimSize_t, DIM>> {
+                public Registrable<Conv_Op<DIM>, std::string, std::shared_ptr<OperatorImpl>(const Conv_Op<DIM> &)>,
+                public StaticAttributes<ConvAttr,
+                                        std::array<DimSize_t, DIM>,
+                                        std::array<DimSize_t, DIM>,
+                                        DimSize_t,
+                                        DimSize_t,
+                                        std::array<DimSize_t, DIM>,
+                                        bool> {
 
 public:
     static const std::string Type;
 
     Conv_Op() = delete;
 
-    using Attributes_ = StaticAttributes<ConvAttr, std::array<DimSize_t, DIM>, std::array<DimSize_t, DIM>,
-                                             DimSize_t, DimSize_t, std::array<DimSize_t, DIM>>;
+    using Attributes_ = StaticAttributes<ConvAttr,
+                                        std::array<DimSize_t, DIM>,
+                                        std::array<DimSize_t, DIM>,
+                                        DimSize_t,
+                                        DimSize_t,
+                                        std::array<DimSize_t, DIM>,
+                                        bool>;
     template <ConvAttr e>
     using attr = typename Attributes_::template attr<e>;
 
@@ -49,13 +62,15 @@ public:
                       DimSize_t outChannels,
                       const std::array<DimSize_t, DIM> &kernelDims,
                       const std::array<DimSize_t, DIM> &strideDims = create_array<DimSize_t,DIM>(1),
-                      const std::array<DimSize_t, DIM> &dilationDims = create_array<DimSize_t,DIM>(1))
+                      const std::array<DimSize_t, DIM> &dilationDims = create_array<DimSize_t,DIM>(1),
+                      bool noBias = false)
         : OperatorTensor(Type, 1, 2, 1),
           Attributes_(attr<ConvAttr::StrideDims>(strideDims),
                       attr<ConvAttr::DilationDims>(dilationDims),
                       attr<ConvAttr::InChannels>(inChannels),
                       attr<ConvAttr::OutChannels>(outChannels),
-                      attr<ConvAttr::KernelDims>(kernelDims)) {}
+                      attr<ConvAttr::KernelDims>(kernelDims),
+                      attr<ConvAttr::NoBias>(noBias)) {}
 
     /**
      * @brief Copy-constructor. Copy the operator attributes and its output tensor(s), but not its input tensors (the new operator has no input associated).
@@ -65,7 +80,11 @@ public:
         : OperatorTensor(op),
           Attributes_(op)
     {
-        mImpl = op.mImpl ? Registrar<Conv_Op<DIM>>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl) {
+            SET_IMPL_MACRO(Conv_Op<DIM>, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -94,7 +113,7 @@ public:
         bool associated = true;
         for (IOIndex_t i = 0; i < 3; ++i) {
             if (!getInput(i)) {
-                AIDGE_THROW_OR_ABORT(std::runtime_error, "Every input should be associated with a Tensor");
+                AIDGE_THROW_OR_ABORT(std::runtime_error, "{}: input #{} should be associated with a Tensor", type(), i);
             }
             associated &= !(getInput(i)->empty());
         }
@@ -118,8 +137,10 @@ public:
         }
     }
 
-
-std::vector<std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>> computeReceptiveField(const std::vector<DimSize_t>& firstEltDims, const std::vector<DimSize_t>& outputDims, const IOIndex_t outputIdx = 0) const override {
+    std::vector<std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>>
+    computeReceptiveField(const std::vector<DimSize_t>& firstEltDims,
+                          const std::vector<DimSize_t>& outputDims,
+                          const IOIndex_t outputIdx = 0) const override {
         if (outputIdx != 0) {
             AIDGE_THROW_OR_ABORT(std::runtime_error, "Conv_Op Operator has got only one output Tensor.");
         }
@@ -133,7 +154,7 @@ std::vector<std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>> co
 
             for (DimIdx_t i = 0; i < (DIM+2); ++i) {
                 if (((outputDims[i] + firstEltDims[i]) > mOutputs[0]->template dims<DIM+2>()[i]) || (outputDims[i] == 0)) {
-                    AIDGE_THROW_OR_ABORT(std::runtime_error, "Given outputDim out of range for dimension %lu (%lu + %lu)", static_cast<std::size_t>(i), firstEltDims[i], outputDims[i]);
+                    AIDGE_THROW_OR_ABORT(std::runtime_error, "Given outputDim out of range for dimension {} ({} + {})", static_cast<std::size_t>(i), firstEltDims[i], outputDims[i]);
                 }
             }
 
@@ -159,22 +180,25 @@ std::vector<std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>> co
             std::vector<DimSize_t> weightIdxDims = std::vector<DimSize_t>(DIM+2, 0);
             weightIdxDims[0] = firstEltDims[1];
 
-            // Bias
-            const std::vector<DimSize_t> biasDims{outputDims[1]}; // the number of output channel
-            const std::vector<DimSize_t> biasIdxDims{firstEltDims[1]};
-
             // Result
             std::vector<std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>> res;
             res.push_back(std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>(inputIdxDims, inputDims));
             res.push_back(std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>(weightIdxDims, weightDims));
-            res.push_back(std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>(biasIdxDims, biasDims));
+
+            // Bias
+            if (! this->template getAttr<ConvAttr::NoBias>()){
+                const std::vector<DimSize_t> biasDims{outputDims[1]}; // the number of output channel
+                const std::vector<DimSize_t> biasIdxDims{firstEltDims[1]};
+                res.push_back(std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>(biasIdxDims, biasDims));
+            }
             return res;
         }
         AIDGE_THROW_OR_ABORT(std::runtime_error, "Given outputDim out of range or output dim not forwarded yet.");
     }
 
+
     void setBackend(const std::string &name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Conv_Op<DIM>>::create(name)(*this);
+        SET_IMPL_MACRO(Conv_Op<DIM>, *this, name);
         mOutputs[0]->setBackend(name, device);
 
         // By default, automatically set backend for weight and bias inputs
@@ -211,12 +235,14 @@ inline std::shared_ptr<Node> Conv(DimSize_t inChannels,
                                   const std::array<DimSize_t, DIM> &kernelDims,
                                   const std::string& name = "",
                                   const std::array<DimSize_t, DIM> &strideDims = create_array<DimSize_t,DIM>(1),
-                                  const std::array<DimSize_t, DIM> &dilationDims = create_array<DimSize_t,DIM>(1)) {
+                                  const std::array<DimSize_t, DIM> &dilationDims = create_array<DimSize_t,DIM>(1),
+                                  bool noBias = false) {
     // FIXME: properly handle default w&b initialization in every cases
     static_assert(DIM<=MaxDim,"Too many kernel dimensions required by Conv, not supported");
-    auto conv = std::make_shared<Node>(std::make_shared<Conv_Op<static_cast<DimIdx_t>(DIM)>>(inChannels, outChannels, kernelDims, strideDims, dilationDims), name);
+    auto conv = std::make_shared<Node>(std::make_shared<Conv_Op<static_cast<DimIdx_t>(DIM)>>(inChannels, outChannels, kernelDims, strideDims, dilationDims, noBias), name);
     addProducer(conv, 1, append(outChannels, append(inChannels, kernelDims)), "w");
-    addProducer(conv, 2, {outChannels}, "b");
+    addProducer(conv, 2, {(noBias ? 0 : outChannels)}, "b"); // already sets bias dims
+
     return conv;
 }
 
@@ -228,9 +254,10 @@ inline std::shared_ptr<Node> Conv(
     DimSize_t const (&kernelDims)[DIM],
     const std::string& name = "",
     const std::array<DimSize_t, DIM> &strideDims = create_array<DimSize_t,DIM>(1),
-    const std::array<DimSize_t, DIM> &dilationDims = create_array<DimSize_t,DIM>(1)) {
+    const std::array<DimSize_t, DIM> &dilationDims = create_array<DimSize_t,DIM>(1),
+    bool noBias = false) {
     static_assert(DIM<=MaxDim,"Too many kernel dimensions required by Conv, not supported");
-    return Conv(inChannels, outChannels, to_array(kernelDims), name, strideDims, dilationDims);
+    return Conv(inChannels, outChannels, to_array(kernelDims), name, strideDims, dilationDims, noBias);
 }
 }  // namespace Aidge
 
@@ -241,8 +268,9 @@ const char *const EnumStrings<Aidge::ConvAttr>::data[] = {
     "DilationDims",
     "InChannels",
     "OutChannels",
-    "KernelDims"
+    "KernelDims",
+    "NoBias"
 };
 }
 
-#endif /* AIDGE_CORE_OPERATOR_CONV_H_ */
\ No newline at end of file
+#endif /* AIDGE_CORE_OPERATOR_CONV_H_ */
diff --git a/include/aidge/operator/ConvDepthWise.hpp b/include/aidge/operator/ConvDepthWise.hpp
index 9d0c0bf408a2f634f96881cd339c330340d5e344..559c0fc7a97a3a882f6720a91d02dee1af70abd8 100644
--- a/include/aidge/operator/ConvDepthWise.hpp
+++ b/include/aidge/operator/ConvDepthWise.hpp
@@ -13,29 +13,33 @@
 #define AIDGE_CORE_OPERATOR_CONVDEPTHWISE_H_
 
 #include <array>
-#include <cmath>
-#include <numeric>
+#include <cmath>    // std::floor
+#include <cstddef>  // std::size_t
+#include <string>
+#include <utility>  // std::pair
 #include <vector>
 
 #include "aidge/data/Tensor.hpp"
 #include "aidge/graph/Node.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/operator/Producer.hpp"
+#include "aidge/utils/ArrayHelpers.hpp"
 #include "aidge/utils/StaticAttributes.hpp"
 #include "aidge/utils/Registrar.hpp"
 #include "aidge/utils/Types.h"
 
 namespace Aidge {
-enum class ConvDepthWiseAttr { StrideDims, DilationDims, Channels, KernelDims };
+enum class ConvDepthWiseAttr { StrideDims, DilationDims, Channels, KernelDims, NoBias };
 
 template <DimIdx_t DIM>
 class ConvDepthWise_Op : public OperatorTensor,
-                public Registrable<ConvDepthWise_Op<DIM>, std::string, std::unique_ptr<OperatorImpl>(const ConvDepthWise_Op<DIM> &)>,
+                public Registrable<ConvDepthWise_Op<DIM>, std::string, std::shared_ptr<OperatorImpl>(const ConvDepthWise_Op<DIM> &)>,
                 public StaticAttributes<ConvDepthWiseAttr,
                                        std::array<DimSize_t, DIM>,
                                        std::array<DimSize_t, DIM>,
                                        DimSize_t,
-                                       std::array<DimSize_t, DIM>> {
+                                       std::array<DimSize_t, DIM>,
+                                       bool> {
 public:
     static const std::string Type;
 
@@ -45,19 +49,22 @@ public:
                                              std::array<DimSize_t, DIM>,
                                              std::array<DimSize_t, DIM>,
                                              DimSize_t,
-                                             std::array<DimSize_t, DIM>>;
+                                             std::array<DimSize_t, DIM>,
+                                             bool>;
     template <ConvDepthWiseAttr e>
     using attr = typename Attributes_::template attr<e>;
 
     constexpr ConvDepthWise_Op(const DimSize_t nbChannels,
                                const std::array<DimSize_t, DIM> &kernel_dims,
                                const std::array<DimSize_t, DIM> &stride_dims = create_array<DimSize_t,DIM>(1),
-                               const std::array<DimSize_t, DIM> &dilation_dims = create_array<DimSize_t,DIM>(1))
+                               const std::array<DimSize_t, DIM> &dilation_dims = create_array<DimSize_t,DIM>(1),
+                               bool no_bias=false)
         : OperatorTensor(Type, 1, 2, 1),
           Attributes_(attr<ConvDepthWiseAttr::StrideDims>(stride_dims),
                       attr<ConvDepthWiseAttr::DilationDims>(dilation_dims),
                       attr<ConvDepthWiseAttr::Channels>(nbChannels),
-                      attr<ConvDepthWiseAttr::KernelDims>(kernel_dims)) {}
+                      attr<ConvDepthWiseAttr::KernelDims>(kernel_dims),
+                      attr<ConvDepthWiseAttr::NoBias>(no_bias)) {}
 
     /**
      * @brief Copy-constructor. Copy the operator attributes and its output tensor(s), but not its input tensors (the new operator has no input associated).
@@ -67,7 +74,11 @@ public:
         : OperatorTensor(op),
           Attributes_(op)
     {
-        mImpl = op.mImpl ? Registrar<ConvDepthWise_Op<DIM>>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl){
+            SET_IMPL_MACRO(ConvDepthWise_Op<DIM>, *this, op.backend());
+        }else{
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -85,7 +96,7 @@ public:
         bool associated = true;
         for (IOIndex_t i = 0; i < 3; ++i) {
             if (!getInput(i)) {
-                AIDGE_THROW_OR_ABORT(std::runtime_error, "Every input should be associated with a Tensor");
+                AIDGE_THROW_OR_ABORT(std::runtime_error, "{}: input #{} should be associated with a Tensor", type(), i);
             }
             associated &= !(getInput(i)->empty());
         }
@@ -128,7 +139,7 @@ public:
 
             for (DimIdx_t i = 0; i < (DIM+2); ++i) {
                 if (((outputDims[i] + firstEltDims[i]) > mOutputs[0]->template dims<DIM+2>()[i]) || (outputDims[i] == 0)) {
-                    AIDGE_THROW_OR_ABORT(std::runtime_error, "Given outputDim out of range for dimension %lu (%lu + %lu)", static_cast<std::size_t>(i), firstEltDims[i], outputDims[i]);
+                    AIDGE_THROW_OR_ABORT(std::runtime_error, "Given outputDim out of range for dimension {} ({} + {})", static_cast<std::size_t>(i), firstEltDims[i], outputDims[i]);
                 }
             }
 
@@ -153,22 +164,24 @@ public:
             std::vector<DimSize_t> weightIdxDims = std::vector<DimSize_t>(DIM+2, 0);
             weightIdxDims[0] = firstEltDims[1];
 
-            // Bias
-            const std::vector<DimSize_t> biasDims{outputDims[1]}; // the number of output channel
-            const std::vector<DimSize_t> biasIdxDims{firstEltDims[1]};
 
             // Result
             std::vector<std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>> res;
             res.push_back(std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>(inputIdxDims, inputDims));
             res.push_back(std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>(weightIdxDims, weightDims));
-            res.push_back(std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>(biasIdxDims, biasDims));
+            // Bias
+            if (! this->template getAttr<ConvDepthWiseAttr::NoBias>()){
+                const std::vector<DimSize_t> biasDims{outputDims[1]}; // the number of output channel
+                const std::vector<DimSize_t> biasIdxDims{firstEltDims[1]};
+                res.push_back(std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>(biasIdxDims, biasDims));
+            }
             return res;
         }
         AIDGE_THROW_OR_ABORT(std::runtime_error, "Given outputDim out of range or output dim not forwarded yet.");
     }
 
     void setBackend(const std::string &name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<ConvDepthWise_Op<DIM>>::create(name)(*this);
+        SET_IMPL_MACRO(ConvDepthWise_Op<DIM>, *this, name);
         mOutputs[0]->setBackend(name, device);
 
         // By default, automatically set backend for weight and bias inputs
@@ -192,12 +205,13 @@ inline std::shared_ptr<Node> ConvDepthWise(const DimSize_t nbChannels,
                                            const std::array<DimSize_t, DIM> &kernelDims,
                                            const std::string& name = "",
                                            const std::array<DimSize_t, DIM> &strideDims = create_array<DimSize_t,DIM>(1),
-                                           const std::array<DimSize_t, DIM> &dilationDims = create_array<DimSize_t,DIM>(1)) {
+                                           const std::array<DimSize_t, DIM> &dilationDims = create_array<DimSize_t,DIM>(1),
+                                           bool noBias=false) {
     // FIXME: properly handle default w&b initialization in every cases
     static_assert(DIM<=MaxDim,"Too many kernel dimensions required by ConvDepthWise, not supported");
-    auto convDW = std::make_shared<Node>(std::make_shared<ConvDepthWise_Op<static_cast<DimIdx_t>(DIM)>>(nbChannels, kernelDims, strideDims, dilationDims), name);
+    auto convDW = std::make_shared<Node>(std::make_shared<ConvDepthWise_Op<static_cast<DimIdx_t>(DIM)>>(nbChannels, kernelDims, strideDims, dilationDims, noBias), name);
     addProducer(convDW, 1, append(nbChannels, append(DimSize_t(1), kernelDims)), "w");
-    addProducer(convDW, 2, {nbChannels}, "b");
+    addProducer(convDW, 2, {(noBias ? 0 : nbChannels)}, "b");
     return convDW;
 }
 
@@ -208,16 +222,17 @@ inline std::shared_ptr<Node> ConvDepthWise(
     DimSize_t const (&kernelDims)[DIM],
     const std::string& name = "",
     const std::array<DimSize_t, DIM> &strideDims = create_array<DimSize_t,DIM>(1),
-    const std::array<DimSize_t, DIM> &dilationDims = create_array<DimSize_t,DIM>(1)) {
+    const std::array<DimSize_t, DIM> &dilationDims = create_array<DimSize_t,DIM>(1),
+    bool noBias=false) {
     static_assert(DIM<=MaxDim,"Too many kernel dimensions required by ConvDepthWise, not supported");
-    return ConvDepthWise(nbChannels, to_array(kernelDims), name, strideDims, dilationDims);
+    return ConvDepthWise(nbChannels, to_array(kernelDims), name, strideDims, dilationDims, noBias);
 }
 }  // namespace Aidge
 
 namespace {
 template <>
 const char *const EnumStrings<Aidge::ConvDepthWiseAttr>::data[] = {"StrideDims", "DilationDims", "Channels",
-                                                          "KernelDims"};
+                                                          "KernelDims", "NoBias"};
 }
 
 #endif /* AIDGE_CORE_OPERATOR_CONVDEPTHWISE_H_ */
diff --git a/include/aidge/operator/Div.hpp b/include/aidge/operator/Div.hpp
index 94b755e0fdb0f76d54cd4f046fb8b08dda05b6b2..49410db044518dc3ca2cc33285d570197d83b10a 100644
--- a/include/aidge/operator/Div.hpp
+++ b/include/aidge/operator/Div.hpp
@@ -12,21 +12,20 @@
 #ifndef AIDGE_CORE_OPERATOR_DIV_H_
 #define AIDGE_CORE_OPERATOR_DIV_H_
 
-#include <cassert>
 #include <memory>
+#include <string>
 #include <vector>
 
 #include "aidge/utils/Registrar.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/backend/OperatorImpl.hpp"
-#include "aidge/data/Tensor.hpp"
 #include "aidge/graph/Node.hpp"
 #include "aidge/utils/Types.h"
 
 namespace Aidge {
 
 class Div_Op : public OperatorTensor,
-    public Registrable<Div_Op, std::string, std::unique_ptr<OperatorImpl>(const Div_Op&)> {
+    public Registrable<Div_Op, std::string, std::shared_ptr<OperatorImpl>(const Div_Op&)> {
 
 public:
     static const std::string Type;
@@ -40,7 +39,11 @@ public:
     Div_Op(const Div_Op& op)
         : OperatorTensor(op)
     {
-        mImpl = op.mImpl ? Registrar<Div_Op>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl) {
+            SET_IMPL_MACRO(Div_Op, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -53,14 +56,10 @@ public:
 
     void computeOutputDims() override final;
 
-
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Div_Op>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override;
 
     static const std::vector<std::string> getInputsName(){
-        return {"data_input"};
+        return {"data_input_1", "data_input_2"};
     }
     static const std::vector<std::string> getOutputsName(){
         return {"data_output"};
diff --git a/include/aidge/operator/Erf.hpp b/include/aidge/operator/Erf.hpp
index 6995cea5e4af9a17cf3d24516d9840850e701669..5ec10522e889bb1188b2304940fd892c0928b414 100644
--- a/include/aidge/operator/Erf.hpp
+++ b/include/aidge/operator/Erf.hpp
@@ -12,22 +12,20 @@
 #ifndef AIDGE_CORE_OPERATOR_ERF_H_
 #define AIDGE_CORE_OPERATOR_ERF_H_
 
-#include <cassert>
 #include <memory>
+#include <string>
 #include <vector>
 
-#include "aidge/utils/Registrar.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/backend/OperatorImpl.hpp"
-#include "aidge/data/Tensor.hpp"
-#include "aidge/data/Data.hpp"
 #include "aidge/graph/Node.hpp"
+#include "aidge/utils/Registrar.hpp"
 #include "aidge/utils/Types.h"
 
 namespace Aidge {
 
 class Erf_Op : public OperatorTensor,
-    public Registrable<Erf_Op, std::string, std::unique_ptr<OperatorImpl>(const Erf_Op&)> {
+    public Registrable<Erf_Op, std::string, std::shared_ptr<OperatorImpl>(const Erf_Op&)> {
 public:
     static const std::string Type;
 
@@ -40,7 +38,11 @@ public:
     Erf_Op(const Erf_Op& op)
         : OperatorTensor(op)
     {
-        mImpl = op.mImpl ? Registrar<Erf_Op>::create(mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl) {
+            SET_IMPL_MACRO(Erf_Op, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -51,10 +53,7 @@ public:
         return std::make_shared<Erf_Op>(*this);
     }
 
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Erf_Op>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override;
 
     static const std::vector<std::string> getInputsName(){
         return {"data_input"};
diff --git a/include/aidge/operator/FC.hpp b/include/aidge/operator/FC.hpp
index a73734ad20e10fe2a3e1d0d12d40e584b4540fb4..222f0ec1235a946865d1b06948bf8b72c5be5a48 100644
--- a/include/aidge/operator/FC.hpp
+++ b/include/aidge/operator/FC.hpp
@@ -13,13 +13,10 @@
 #define AIDGE_CORE_OPERATOR_FC_H_
 
 #include <array>
-#include <cmath>
-#include <numeric>
 #include <memory>
 #include <vector>
 
 #include "aidge/utils/Types.h"
-#include "aidge/data/Tensor.hpp"
 #include "aidge/graph/Node.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/operator/Producer.hpp"
@@ -32,7 +29,7 @@ enum class FCAttr { OutChannels, NoBias };
 class FC_Op : public OperatorTensor,
               public Registrable<FC_Op,
                                  std::string,
-                                 std::unique_ptr<OperatorImpl>(const FC_Op &)>,
+                                 std::shared_ptr<OperatorImpl>(const FC_Op &)>,
               public StaticAttributes<FCAttr, DimSize_t, bool> {
 public:
     static const std::string Type;
@@ -57,57 +54,31 @@ public:
         : OperatorTensor(op),
           Attributes_(op)
     {
-        mImpl = op.mImpl ? Registrar<FC_Op>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl){
+            SET_IMPL_MACRO(FC_Op, *this, op.backend());
+        }else{
+            mImpl = nullptr;
+        }
     }
 
     /**
      * @brief Clone the operator using its copy-constructor.
      * @see Operator::FC_Op
      */
-    std::shared_ptr<Operator> clone() const override {
+    std::shared_ptr<Operator> clone() const override final {
         return std::make_shared<FC_Op>(*this);
     }
 
-    void associateInput(const IOIndex_t inputIdx, const std::shared_ptr<Data>& data) override final {
-        assert(inputIdx < 3 && "operators supports only 3 inputs");
-        assert(strcmp(data->type(), Tensor::Type)==0 && "input data must be of Tensor type");
-        if (inputIdx == 2) {
-            assert(std::dynamic_pointer_cast<Tensor>(data)->size() == ((this->template getAttr<FCAttr::NoBias>()) == false ? static_cast<std::size_t>(this->template getAttr<FCAttr::OutChannels>()) : 0));
-            assert(std::dynamic_pointer_cast<Tensor>(data)->nbDims() == 1);
-        }
-        mInputs[inputIdx] = std::dynamic_pointer_cast<Tensor>(data);
-        if (inputIdx == 0 && getInput(0)->nbDims() == 1)
-            mInputs[inputIdx]->resize({1, getInput(inputIdx)->size()});
-    }
-
-    void computeOutputDims() override final {
-        bool associated = true;
-        for (IOIndex_t i = 0; i < nbInputs(); ++i) {
-            if (!getInput(i)) {
-                AIDGE_THROW_OR_ABORT(std::runtime_error, "Every input should be associated with a Tensor");
-            }
-            associated &= !(getInput(i)->empty());
-        }
-        if (associated) {
-            // <batch, OutChannels>
-            mOutputs[0]->resize({getInput(0)->dims()[0], this->template getAttr<FCAttr::OutChannels>()});
-        }
-    }
+    void associateInput(const IOIndex_t inputIdx, const std::shared_ptr<Data>& data) override final;
 
+    void computeOutputDims() override final;
 
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<FC_Op>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-
-        // By default, automatically set backend for weight and bias inputs
-        getInput(1)->setBackend(name, device);
-        getInput(2)->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override;
 
-    static const std::vector<std::string> getInputsName(){
+    static const std::vector<std::string> getInputsName() {
         return {"data_input", "weight", "bias"};
     }
-    static const std::vector<std::string> getOutputsName(){
+    static const std::vector<std::string> getOutputsName() {
         return {"data_output"};
     }
 };
@@ -127,4 +98,4 @@ const char *const EnumStrings<Aidge::FCAttr>::data[] = {"OutChannels",
                                                         "NoBias"};
 }
 
-#endif /* AIDGE_CORE_OPERATOR_FC_H_ */
\ No newline at end of file
+#endif /* AIDGE_CORE_OPERATOR_FC_H_ */
diff --git a/include/aidge/operator/Gather.hpp b/include/aidge/operator/Gather.hpp
index 20082eed28825ade9d62fb5d4e081840d3bd4442..b7d18e6443404730bbcb73cf7e6da97b8b3e6a7c 100644
--- a/include/aidge/operator/Gather.hpp
+++ b/include/aidge/operator/Gather.hpp
@@ -12,40 +12,39 @@
 #ifndef AIDGE_CORE_OPERATOR_GATHER_H_
 #define AIDGE_CORE_OPERATOR_GATHER_H_
 
-#include <cassert>
+#include <cstdint>  // std::int64_t
 #include <memory>
+#include <string>
 #include <vector>
 
 #include "aidge/backend/OperatorImpl.hpp"
-#include "aidge/data/Tensor.hpp"
-#include "aidge/data/Data.hpp"
 #include "aidge/graph/Node.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
-#include "aidge/operator/Producer.hpp"
 #include "aidge/utils/Registrar.hpp"
 #include "aidge/utils/StaticAttributes.hpp"
 #include "aidge/utils/Types.h"
 
 namespace Aidge {
-enum class GatherAttr { Axis };
+enum class GatherAttr { Indices, GatheredShape, Axis };
 
 class Gather_Op : public OperatorTensor,
                 public Registrable<Gather_Op,
                                    std::string,
-                                   std::unique_ptr<OperatorImpl>(const Gather_Op&)>,
-                public StaticAttributes<GatherAttr, int> {
+                                   std::shared_ptr<OperatorImpl>(const Gather_Op&)>,
+                public StaticAttributes<GatherAttr, std::vector<std::int64_t>, std::vector<DimSize_t>, std::int64_t> {
 
 public:
     static const std::string Type;
 
     Gather_Op() = delete;
 
-
-    using Attributes_ = StaticAttributes<GatherAttr, int>;
+    using Attributes_ = StaticAttributes<GatherAttr, std::vector<std::int64_t>, std::vector<DimSize_t>, std::int64_t>;
     template <GatherAttr e> using attr = typename Attributes_::template attr<e>;
-    Gather_Op(int axis)
-            : OperatorTensor(Type, 2, 0, 1),
+    Gather_Op(const std::vector<std::int64_t>& indices, const std::vector<DimSize_t>& gatheredShape, std::int64_t axis)
+            : OperatorTensor(Type, 1, 0, 1),
             Attributes_(
+                attr<GatherAttr::Indices>(indices),
+                attr<GatherAttr::GatheredShape>(gatheredShape),
                 attr<GatherAttr::Axis>(axis))
     {}
 
@@ -57,7 +56,11 @@ public:
         : OperatorTensor(op),
           Attributes_(op)
     {
-        mImpl = op.mImpl ? Registrar<Gather_Op>::create(mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl){
+            SET_IMPL_MACRO(Gather_Op, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -70,27 +73,24 @@ public:
 
     void computeOutputDims() override final;
 
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Gather_Op>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override;
 
     static const std::vector<std::string> getInputsName(){
-        return {"data_input", "indexes"};
+        return {"data_input"};
     }
     static const std::vector<std::string> getOutputsName(){
         return {"data_output"};
     }
 };
 
-inline std::shared_ptr<Node> Gather(int axis = 0, const std::string& name = "") {
-    return std::make_shared<Node>(std::make_shared<Gather_Op>(axis), name);
+inline std::shared_ptr<Node> Gather( const std::vector<std::int64_t>& indices, const std::vector<DimSize_t>& gatheredShape, std::int64_t axis = 0, const std::string& name = "") {
+    return std::make_shared<Node>(std::make_shared<Gather_Op>(indices, gatheredShape, axis), name);
 }
 } // namespace Aidge
 
 namespace {
 template <>
-const char *const EnumStrings<Aidge::GatherAttr>::data[] = {"Axis"};
+const char *const EnumStrings<Aidge::GatherAttr>::data[] = {"Indices", "GatheredShape", "Axis"};
 }
 
 #endif /* AIDGE_CORE_OPERATOR_GATHER_H_ */
diff --git a/include/aidge/operator/GenericOperator.hpp b/include/aidge/operator/GenericOperator.hpp
index c966b5f5c1bb4914f3e46f96493da87a6707b1ff..e7d60285b4d45826f1d73635d54f4532b4fb1598 100644
--- a/include/aidge/operator/GenericOperator.hpp
+++ b/include/aidge/operator/GenericOperator.hpp
@@ -15,8 +15,6 @@
 #include <memory>
 #include <vector>
 #include <string>
-#include <cassert>
-#include <cstring>
 
 #include "aidge/graph/Node.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
@@ -38,7 +36,9 @@ private:
 public:
     GenericOperator_Op(const std::string& type, IOIndex_t nbData, IOIndex_t nbParam, IOIndex_t nbOut)
         : OperatorTensor(type, nbData, nbParam, nbOut)
-    {}
+    {
+        mImpl = std::make_shared<OperatorImpl>(*this, "");
+    }
 
     /**
      * @brief Copy-constructor. Copy the operator attributes and its output tensor(s), but not its input tensors (the new operator has no input associated).
@@ -46,7 +46,11 @@ public:
      */
     GenericOperator_Op(const GenericOperator_Op& op)
         : OperatorTensor(op)
-    {}
+    {
+        mImpl = std::make_shared<OperatorImpl>(*this, op.backend());
+    }
+
+    ~GenericOperator_Op() = default;
 
     /**
      * @brief Clone the operator using its copy-constructor.
@@ -56,62 +60,19 @@ public:
         return std::make_shared<GenericOperator_Op>(*this);
     }
 
-    // Helper functions that can be used with setComputeOutputDims():
-    static const ComputeDimsFunc Identity;
-
-    void setComputeOutputDims(ComputeDimsFunc func) {
-        mComputeOutputDims = func;
-    }
-
-
-    void computeOutputDims() override final {
-        if (mComputeOutputDims) {
-            std::vector<std::vector<size_t>> inputsDims(nbInputs(), std::vector<size_t>());
-            for (std::size_t i = 0; i < nbInputs(); ++i) {
-                if (getInput(i)) {
-                    inputsDims[i] = getInput(i)->dims();
-                }
-            }
-
-            const auto& outputsDims = mComputeOutputDims(inputsDims);
-            assert(outputsDims.size() == nbOutputs() && "The provided ComputeDimsFunc function returns the wrong number of outputs");
-            for (std::size_t i = 0; i < nbOutputs(); ++i) {
-                mOutputs[i]->resize(outputsDims[i]);
-            }
-        }
-        else {
-            assert(false && "Cannot compute output dim of a GenericOperator");
-        }
-    }
-
-    bool outputDimsForwarded() const override final {
-        if (mComputeOutputDims) {
-            return !(mOutputs[0]->empty());
-        }
-        else {
-            assert(false && "GenericOperator cannot forward dims");
-            return false;
-        }
-    }
+public:
+    void computeOutputDims() override final;
 
+    bool outputDimsForwarded() const override final;
 
-    ~GenericOperator_Op() = default;
+    void setBackend(const std::string & /*name*/, DeviceIdx_t /*device*/ = 0) override { fmt::print("setBackend: not available yet.\n"); }
+    void setDataType(const DataType& /*datatype*/) const override { fmt::print("setDataType: not available yet.\n"); }
 
-    void setBackend(const std::string & /*name*/, DeviceIdx_t /*device*/ = 0) override { printf("setBackend: not available yet.\n"); }
-    void setDataType(const DataType& /*datatype*/) const override { printf("setDataType: not available yet.\n"); }
-    void forward() override final {
-        if(mImpl){
-            mImpl->forward();
-        }else{
-            printf("forward: No implementation is linked.\n");
-        }
-    }
-    void backward() override final {
-        if(mImpl){
-            mImpl->backward();
-        }else{
-            printf("backward: No implementation is linked.\n");
-        }
+    // Helper functions that can be used with setComputeOutputDims():
+    static const ComputeDimsFunc Identity;
+    static const ComputeDimsFunc InputIdentity(IOIndex_t inputIdx, IOIndex_t nbOutputs);
+    inline void setComputeOutputDims(ComputeDimsFunc func) {
+        mComputeOutputDims = func;
     }
 };
 
@@ -119,8 +80,8 @@ public:
  * @brief Fictive custom operator not associated with any implementation.
  * Allows to import unknown operators and simulate new ones.
  * @param type Type of the fictive operator.
- * @param nbDataIn Number of input data.
- * @param nbIn Number input data + number of learnt parameters.
+ * @param nbData Number of input data.
+ * @param nbParam Number of parameters.
  * @param nbOut Number of output data.
  * @param name (optional) name of the Operator.
  * @return std::shared_ptr<Node> Node associated with the Generic Operator.
diff --git a/include/aidge/operator/GlobalAveragePooling.hpp b/include/aidge/operator/GlobalAveragePooling.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..12c8eb02d9488edeb760b6a063cfac5f8257db18
--- /dev/null
+++ b/include/aidge/operator/GlobalAveragePooling.hpp
@@ -0,0 +1,74 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CORE_OPERATOR_GLOBAL_AVERAGE_POOLING_H_
+#define AIDGE_CORE_OPERATOR_GLOBAL_AVERAGE_POOLING_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/graph/Node.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
+
+namespace Aidge {
+
+/**
+ * @brief Description for the tensor data structure.
+ * @details Sets the properties of the tensor without actually containing any
+ * data. Contains a pointer to an actual contiguous implementation of data.
+ */
+class GlobalAveragePooling_Op
+    : public OperatorTensor,
+      public Registrable<GlobalAveragePooling_Op, std::string,
+                         std::shared_ptr<OperatorImpl>(
+                             const GlobalAveragePooling_Op &)> {
+public:
+  static const std::string Type;
+
+  GlobalAveragePooling_Op() : OperatorTensor(Type, 1, 0, 1) {}
+
+  GlobalAveragePooling_Op(const GlobalAveragePooling_Op &op)
+      : OperatorTensor(op) {
+        if (op.mImpl) {
+            SET_IMPL_MACRO(GlobalAveragePooling_Op, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
+  }
+
+  std::shared_ptr<Operator> clone() const override {
+    return std::make_shared<GlobalAveragePooling_Op>(*this);
+  }
+
+  void computeOutputDims() override final;
+
+  void setBackend(const std::string &name, DeviceIdx_t device = 0) override final;
+
+  static const std::vector<std::string> getInputsName() {
+    return {"data_input"};
+  }
+  static const std::vector<std::string> getOutputsName() {
+    return {"data_output"};
+  }
+};
+
+inline std::shared_ptr<Node>
+GlobalAveragePooling(const std::string &name = "") {
+  return std::make_shared<Node>(std::make_shared<GlobalAveragePooling_Op>(),
+                                name);
+}
+} // namespace Aidge
+
+#endif /* AIDGE_CORE_OPERATOR_GLOBAL_AVERAGE_POOLING_H_ */
diff --git a/include/aidge/operator/Identity.hpp b/include/aidge/operator/Identity.hpp
index 57cd20311a4e4c98966af0af98b9fe4533155ea6..27432bc5bb251003e9e93261593e12c2fa704f3d 100644
--- a/include/aidge/operator/Identity.hpp
+++ b/include/aidge/operator/Identity.hpp
@@ -40,9 +40,9 @@ public:
     static const std::string Type;
 
     Identity_Op()
-            : OperatorTensor(Type, 1, 0, 1)
+        : OperatorTensor(Type, 1, 0, 1)
     {
-        mImpl = std::make_shared<OperatorImpl>(*this);
+        mImpl = std::make_shared<OperatorImpl>(*this, "");
     }
 
     /**
@@ -52,7 +52,7 @@ public:
     Identity_Op(const Identity_Op& op)
         : OperatorTensor(op)
     {
-        mImpl = std::make_shared<OperatorImpl>(*this);
+        mImpl = std::make_shared<OperatorImpl>(*this, op.backend());
     }
 
     /**
@@ -65,11 +65,16 @@ public:
 
     void computeOutputDims() override final {} // Do nothing
 
+    /**
+     * @brief Check if output dimensions have been computed.
+     * @note Since Indentity has no output Tensor, this function checks if its
+     * only input's dimensions have been computed.
+     *
+     * @return true Input has dimensions.
+     * @return false Input has no dimensions or is a nullptr.
+     */
     bool outputDimsForwarded() const override final {
-        if (mInputs[0])
-            return !mInputs[0]->empty();
-        else
-            return false;
+        return mInputs[0] ? !mInputs[0]->empty() : false;
     }
 
 
@@ -78,29 +83,19 @@ public:
     void backward() override final { }
 
     void setOutput(const IOIndex_t outputIdx, const std::shared_ptr<Data>& data) override final {
-        if (strcmp(data->type(), "Tensor") != 0) {
-            AIDGE_THROW_OR_ABORT(std::runtime_error, "%s Operator only accepts Tensors as outputs", type().c_str());
-        }
-        if (outputIdx >= nbInputs()) {
-            AIDGE_THROW_OR_ABORT(std::runtime_error, "%s Operator has %hu outputs", type().c_str(), nbInputs());
-        }
+        AIDGE_ASSERT(data->type() == "Tensor", "{} Operator only accepts Tensors as outputs", type());
+        AIDGE_ASSERT(outputIdx < nbInputs(), "{} Operator has {} outputs", type(), nbInputs());
         *mInputs[outputIdx] = *std::dynamic_pointer_cast<Tensor>(data);
     }
 
     void setOutput(const IOIndex_t outputIdx, std::shared_ptr<Data>&& data) override final {
-        if (strcmp(data->type(), "Tensor") != 0) {
-            AIDGE_THROW_OR_ABORT(std::runtime_error, "%s Operator only accepts Tensors as inputs", type().c_str());
-        }
-        if (outputIdx >= nbInputs()) {
-            AIDGE_THROW_OR_ABORT(std::runtime_error, "%s Operator has %hu outputs", type().c_str(), nbInputs());
-        }
+        AIDGE_ASSERT(data->type() == "Tensor", "{} Operator only accepts Tensors as inputs", type());
+        AIDGE_ASSERT(outputIdx < nbInputs(), "{} Operator has {} outputs", type(), nbInputs());
         *mInputs[outputIdx] = std::move(*std::dynamic_pointer_cast<Tensor>(data));
     }
 
     const std::shared_ptr<Tensor>& getOutput(const IOIndex_t outputIdx) const override final {
-        if (outputIdx >= nbInputs()) {
-            AIDGE_THROW_OR_ABORT(std::runtime_error, "%s Operator has %hu outputs", type().c_str(), nbInputs());
-        }
+        AIDGE_ASSERT(outputIdx < nbInputs(), "{} Operator has {} outputs", type(), nbInputs());
         if (mInputs[outputIdx] == nullptr){
             return mOutputs[outputIdx]; // Input is not initialized with empty tensor
         }
diff --git a/include/aidge/operator/LeakyReLU.hpp b/include/aidge/operator/LeakyReLU.hpp
index 5976f1d88d70ae7fb716f4038e57da95242c3551..83a7c30fce7e0f68576f367d4b0bfe48edf4b3b6 100644
--- a/include/aidge/operator/LeakyReLU.hpp
+++ b/include/aidge/operator/LeakyReLU.hpp
@@ -30,7 +30,7 @@ enum class LeakyReLUAttr {
 };
 
 class LeakyReLU_Op : public OperatorTensor,
-    public Registrable<LeakyReLU_Op, std::string, std::unique_ptr<OperatorImpl>(const LeakyReLU_Op&)>,
+    public Registrable<LeakyReLU_Op, std::string, std::shared_ptr<OperatorImpl>(const LeakyReLU_Op&)>,
     public StaticAttributes<LeakyReLUAttr, float> {
 public:
     static const std::string Type;
@@ -54,7 +54,11 @@ public:
         : OperatorTensor(op),
           Attributes_(op)
     {
-        mImpl = op.mImpl ? Registrar<LeakyReLU_Op>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl){
+            SET_IMPL_MACRO(LeakyReLU_Op, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -68,7 +72,7 @@ public:
 
 
     void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<LeakyReLU_Op>::create(name)(*this);
+        SET_IMPL_MACRO(LeakyReLU_Op, *this, name);
         mOutputs[0]->setBackend(name, device);
     }
 
diff --git a/include/aidge/operator/MatMul.hpp b/include/aidge/operator/MatMul.hpp
index 3d80193be3f669b00e5a138470269e52d0715780..43bd8b1654206df15cd869cf2d37a216fcc4a733 100644
--- a/include/aidge/operator/MatMul.hpp
+++ b/include/aidge/operator/MatMul.hpp
@@ -12,101 +12,74 @@
 #ifndef AIDGE_CORE_OPERATOR_MATMUL_H_
 #define AIDGE_CORE_OPERATOR_MATMUL_H_
 
-#include <array>
-#include <cmath>
-#include <numeric>
 #include <memory>
+#include <string>
 #include <vector>
 
 #include "aidge/utils/Types.h"
-#include "aidge/data/Tensor.hpp"
 #include "aidge/graph/Node.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
-#include "aidge/operator/Producer.hpp"
-#include "aidge/utils/StaticAttributes.hpp"
 #include "aidge/utils/Registrar.hpp"
 
 namespace Aidge {
-enum class MatMulAttr { OutChannels };
 
 class MatMul_Op : public OperatorTensor,
               public Registrable<MatMul_Op,
                                  std::string,
-                                 std::unique_ptr<OperatorImpl>(const MatMul_Op &)>,
-              public StaticAttributes<MatMulAttr, DimSize_t> {
+                                 std::shared_ptr<OperatorImpl>(const MatMul_Op &)> {
 public:
     static const std::string Type;
 
-    MatMul_Op() = delete;
-
-    using Attributes_ = StaticAttributes<MatMulAttr, DimSize_t>;
-    template <MatMulAttr e> using attr = typename Attributes_::template attr<e>;
-
-    MatMul_Op(DimSize_t out_channels)
-            : OperatorTensor(Type, 1, 1, 1),
-            Attributes_(
-                attr<MatMulAttr::OutChannels>(out_channels))
-    {}
+    MatMul_Op() : OperatorTensor(Type, 2, 0, 1) {}
 
     /**
      * @brief Copy-constructor. Copy the operator attributes and its output tensor(s), but not its input tensors (the new operator has no input associated).
      * @param op Operator to copy.
      */
-    MatMul_Op(const MatMul_Op& op)
-        : OperatorTensor(op),
-          Attributes_(op)
+    MatMul_Op(const MatMul_Op& op) : OperatorTensor(op)
     {
-        mImpl = op.mImpl ? Registrar<MatMul_Op>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl){
+            SET_IMPL_MACRO(MatMul_Op, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
     }
 
     /**
      * @brief Clone the operator using its copy-constructor.
      * @see Operator::MatMul_Op
      */
-    std::shared_ptr<Operator> clone() const override {
+    std::shared_ptr<Operator> clone() const override final {
         return std::make_shared<MatMul_Op>(*this);
     }
 
-
-    void computeOutputDims() override final {
-        bool associated = true;
-        for (IOIndex_t i = 0; i < nbInputs(); ++i) {
-            if (!getInput(i)) {
-                AIDGE_THROW_OR_ABORT(std::runtime_error, "Every input should be associated with a Tensor");
-            }
-            associated &= !(getInput(i)->empty());
-        }
-        if (associated) {
-            // <batch, OutChannels>
-            mOutputs[0]->resize({getInput(0)->dims()[0], this->template getAttr<MatMulAttr::OutChannels>()});
-        }
-    }
+    /**
+     * @brief Compute dimensions for the output Tensor following the same rules as
+     * numpy.matmul.
+     * @note - Both inputs are 2-D Tensors: classic matrix multiplication
+     * @note - Either input is N-D with N > 2: it is treated as a stack of matrices residing
+     * in the last two indexes and broadcast accordingly.
+     * @note - First input is 1-D: it is promoted to a matrix by prepending a 1 to its
+     * dimensions (D) -> (1,D). The prepended 1 is removed after computation.
+     * @note - Second input is 1-D: it is promoted to a matrix by appending a 1 to its
+     * dimensions (D) -> (D,1). The appended 1 is removed after computation.
+     */
+    void computeOutputDims() override final;
 
 
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<MatMul_Op>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override final;
 
-    static const std::vector<std::string> getInputsName(){
-        return {"data_input", "weight"};
+    static const std::vector<std::string> getInputsName() {
+        return {"data_input1", "data_input2"};
     }
-    static const std::vector<std::string> getOutputsName(){
+    static const std::vector<std::string> getOutputsName() {
         return {"data_output"};
     }
 };
 
-inline std::shared_ptr<Node> MatMul(DimSize_t inChannels, DimSize_t outChannels, const std::string& name = "") {
-    // FIXME: properly handle default w initialization in every cases
-    auto matmul = std::make_shared<Node>(std::make_shared<MatMul_Op>(outChannels), name);
-    addProducer(matmul, 1, {outChannels, inChannels}, "w");
-    return matmul;
+inline std::shared_ptr<Node> MatMul(const std::string& name = "") {
+    return std::make_shared<Node>(std::make_shared<MatMul_Op>(), name);
 }
 } // namespace Aidge
 
-namespace {
-template <>
-const char *const EnumStrings<Aidge::MatMulAttr>::data[] = {"OutChannels"};
-}
-
-#endif /* AIDGE_CORE_OPERATOR__MATMUL_H_ */
+#endif /* AIDGE_CORE_OPERATOR_MATMUL_H_ */
diff --git a/include/aidge/operator/MaxPooling.hpp b/include/aidge/operator/MaxPooling.hpp
index 467a69d73c98a21c85e956acf42536e197833cbd..5b09aa02cd0665172a9ae69549d8d9311e10d024 100644
--- a/include/aidge/operator/MaxPooling.hpp
+++ b/include/aidge/operator/MaxPooling.hpp
@@ -13,16 +13,20 @@
 #define AIDGE_CORE_OPERATOR_MAXPOOLING_H_
 
 #include <array>
-#include <numeric>
+#include <cmath>       // std::ceil, std::floor
+#include <cstddef>     // std::size_t
+#include <functional>
+#include <memory>
+#include <stdexcept>   // std::runtime_error
 #include <vector>
-#include <cmath>
 
 #include "aidge/data/Tensor.hpp"
 #include "aidge/graph/Node.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
-#include "aidge/operator/Producer.hpp"
-#include "aidge/utils/StaticAttributes.hpp"
+#include "aidge/utils/ArrayHelpers.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
 #include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/StaticAttributes.hpp"
 #include "aidge/utils/Types.h"
 
 namespace Aidge {
@@ -30,7 +34,7 @@ enum class MaxPoolingAttr { StrideDims, KernelDims, CeilMode };
 
 template <DimIdx_t DIM>
 class MaxPooling_Op : public OperatorTensor,
-                public Registrable<MaxPooling_Op<DIM>, std::string, std::unique_ptr<OperatorImpl>(const MaxPooling_Op<DIM> &)>,
+                public Registrable<MaxPooling_Op<DIM>, std::string, std::shared_ptr<OperatorImpl>(const MaxPooling_Op<DIM> &)>,
                 public StaticAttributes<MaxPoolingAttr,
                                        std::array<DimSize_t, DIM>,
                                        std::array<DimSize_t, DIM>,
@@ -64,7 +68,11 @@ public:
         : OperatorTensor(op),
           Attributes_(op)
     {
-        mImpl = op.mImpl ? Registrar<MaxPooling_Op<DIM>>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl) {
+            SET_IMPL_MACRO(MaxPooling_Op<DIM>, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -78,7 +86,7 @@ public:
 
     void computeOutputDims() override final {
         if (!getInput(0)) {
-            AIDGE_THROW_OR_ABORT(std::runtime_error, "Every input should be associated with a Tensor");
+            AIDGE_THROW_OR_ABORT(std::runtime_error, "{}: input #0 should be associated with a Tensor", type());
         }
         if (!(getInput(0)->empty())) {
             std::array<DimSize_t, DIM + 2> outputDims{};
@@ -105,7 +113,7 @@ public:
 
 
     void setBackend(const std::string &name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<MaxPooling_Op<DIM>>::create(name)(*this);
+        SET_IMPL_MACRO(MaxPooling_Op<DIM>, *this, name);
         mOutputs[0]->setBackend(name, device);
     }
 
diff --git a/include/aidge/operator/Memorize.hpp b/include/aidge/operator/Memorize.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..7de34563adcaabd63ab036232d4d7b6539fd11eb
--- /dev/null
+++ b/include/aidge/operator/Memorize.hpp
@@ -0,0 +1,103 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CORE_OPERATOR_MEMORIZE_H_
+#define AIDGE_CORE_OPERATOR_MEMORIZE_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/graph/Node.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/StaticAttributes.hpp"
+#include "aidge/utils/Types.h"
+
+namespace Aidge {
+enum class MemorizeAttr { ScheduleStep, ForwardStep, EndStep };
+
+class Memorize_Op : public OperatorTensor,
+    public Registrable<Memorize_Op, std::string, std::unique_ptr<OperatorImpl>(const Memorize_Op&)>,
+    public StaticAttributes<MemorizeAttr, unsigned int, unsigned int, unsigned int> {
+public:
+    static const std::string Type;
+
+    using Attributes_ = StaticAttributes<MemorizeAttr, unsigned int, unsigned int, unsigned int>;
+    template <MemorizeAttr e>
+    using attr = typename Attributes_::template attr<e>;
+
+    Memorize_Op(const unsigned int endStep)
+        : OperatorTensor(Type, 1, 1, 2),
+          Attributes_(attr<MemorizeAttr::ScheduleStep>(0),
+                      attr<MemorizeAttr::ForwardStep>(0),
+                      attr<MemorizeAttr::EndStep>(endStep))
+    {
+        mOutputs[1] = mOutputs[0];
+    }
+
+    /**
+     * @brief Copy-constructor. Copy the operator attributes and its output tensor(s),
+     * but not its input tensors (the new operator has no input associated).
+     * @param op Operator to copy.
+     */
+    Memorize_Op(const Memorize_Op& op)
+        : OperatorTensor(op),
+          Attributes_(op)
+    {
+        if (op.mImpl) {
+            SET_IMPL_MACRO(Memorize_Op, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
+        mOutputs[1] = mOutputs[0];
+    }
+
+    /**
+     * @brief Clone the operator using its copy-constructor.
+     * @see Operator::Memorize_Op
+     */
+    std::shared_ptr<Operator> clone() const override {
+        return std::make_shared<Memorize_Op>(*this);
+    }
+
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override final;
+
+    void computeOutputDims() override;
+    bool outputDimsForwarded() const override;
+    void updateConsummerProducer() override;
+    void forward() override;
+
+    static const std::vector<std::string> getInputsName(){
+        return {"data_input", "data_input_init"};
+    }
+    static const std::vector<std::string> getOutputsName(){
+        return {"data_output", "data_output_rec"};
+    }
+};
+
+inline std::shared_ptr<Node> Memorize(const unsigned int endStep, const std::string& name = "") {
+    return std::make_shared<Node>(std::make_shared<Memorize_Op>(endStep), name);
+}
+}  // namespace Aidge
+
+namespace {
+template <>
+const char *const EnumStrings<Aidge::MemorizeAttr>::data[] = {
+    "ScheduleStep",
+    "ForwardStep",
+    "EndStep"
+};
+}
+
+#endif /* AIDGE_CORE_OPERATOR_MEMORIZE_H_ */
diff --git a/include/aidge/operator/MetaOperator.hpp b/include/aidge/operator/MetaOperator.hpp
index 5955d860a2e9a0db9bb296552927c40eb411f30d..5ac9cf3c92b1951407e4c1892b1a8dc70a724013 100644
--- a/include/aidge/operator/MetaOperator.hpp
+++ b/include/aidge/operator/MetaOperator.hpp
@@ -12,10 +12,18 @@
 #ifndef AIDGE_CORE_OPERATOR_METAOPERATOR_H_
 #define AIDGE_CORE_OPERATOR_METAOPERATOR_H_
 
-#include "aidge/operator/OperatorTensor.hpp"
+#include <array>
+#include <memory>
+#include <string>
+
+#include "aidge/data/Data.hpp"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/graph/GraphView.hpp"
 #include "aidge/graph/OpArgs.hpp"
-#include "aidge/scheduler/Scheduler.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/scheduler/SequentialScheduler.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
 
 namespace Aidge {
 class MetaOperator_Op : public OperatorTensor,
@@ -25,9 +33,10 @@ public:
     // Micro-graph handling:
     std::shared_ptr<GraphView> mGraph; // Meta operator micro-graph
     std::shared_ptr<SequentialScheduler> mScheduler;
+    std::weak_ptr<Node> mUpperNode;
 
    public:
-    MetaOperator_Op(const char *type, const std::shared_ptr<GraphView>& graph);
+    MetaOperator_Op(const std::string& type, const std::shared_ptr<GraphView>& graph);
 
     /**
      * @brief Copy-constructor. Copy the operator attributes and its output tensor(s), but not its input tensors (the new operator has no input associated).
@@ -38,6 +47,13 @@ public:
           mGraph(op.mGraph->clone())
     {}
 
+    /**
+     * Set the node that should be used for the scheduling.
+    */
+    void setUpperNode(std::shared_ptr<Node> node) {
+        mUpperNode = node;
+    }
+
     /**
      * @brief Clone the operator using its copy-constructor.
      * @see Operator::MetaOperator_Op
@@ -55,7 +71,8 @@ public:
     }
 
     void associateInput(const IOIndex_t inputIdx, const std::shared_ptr<Data>& data) override final {
-        assert(strcmp(data->type(), Tensor::Type) == 0 && "input data must be of Tensor type");
+        AIDGE_ASSERT(data->type() == Tensor::Type, "input data must be of Tensor type");
+        AIDGE_ASSERT(inputIdx < mGraph->getOrderedInputs().size(), "associateInput(): inputIdx ({}) out of bound for MetaOperator", inputIdx);
 
         const auto& inputOp = mGraph->getOrderedInputs()[inputIdx];
         inputOp.first->getOperator()->associateInput(inputOp.second, data);
@@ -65,8 +82,17 @@ public:
     }
 
     void computeOutputDims() override final {
-        // Forward dims of micro-graph
-        mGraph->forwardDims();
+        // Check first that all required inputs are available, otherwise
+        // mGraph->forwardDims() will fail!
+        bool forwarded = true;
+        for (IOIndex_t i = 0; i < nbInputs(); ++i) {
+            forwarded &= mInputs[i] ? !(getInput(i)->empty()) : false;
+        }
+
+        if (forwarded) {
+            // Forward dims of micro-graph
+            mGraph->forwardDims();
+        }
     }
 
 
@@ -89,9 +115,11 @@ public:
         mGraph->setDataType(datatype);
     }
 
-    NbElts_t getNbRequiredData(const IOIndex_t inputIdx) const override;
-    NbElts_t getNbConsumedData(IOIndex_t inputIdx) const override;
-    NbElts_t getNbProducedData(IOIndex_t outputIdx) const override;
+    Elts_t getNbRequiredData(const IOIndex_t inputIdx) const override;
+    Elts_t getNbRequiredProtected(const IOIndex_t inputIdx) const override;
+    Elts_t getRequiredMemory(const IOIndex_t outputIdx, const std::vector<DimSize_t> &inputsSize) const override;
+    Elts_t getNbConsumedData(IOIndex_t inputIdx) const override;
+    Elts_t getNbProducedData(IOIndex_t outputIdx) const override;
 
     void updateConsummerProducer() override;
     void forward() override;
@@ -107,7 +135,10 @@ inline std::shared_ptr<Node> MetaOperator(const char *type,
                                   const std::shared_ptr<GraphView>& graph,
                                   const std::string& name = "")
 {
-    return std::make_shared<Node>(std::make_shared<MetaOperator_Op>(type, graph), name);
+    auto op = std::make_shared<MetaOperator_Op>(type, graph);
+    auto node = std::make_shared<Node>(op, name);
+    op->setUpperNode(node);
+    return node;
 }
 }  // namespace Aidge
 
diff --git a/include/aidge/operator/MetaOperatorDefs.hpp b/include/aidge/operator/MetaOperatorDefs.hpp
index 2832f9fce005e0ae9d2bab98bf764c68f93e3cda..fb3aa6384fc703d758cb8753dcf54c4694f96bd4 100644
--- a/include/aidge/operator/MetaOperatorDefs.hpp
+++ b/include/aidge/operator/MetaOperatorDefs.hpp
@@ -18,6 +18,14 @@
 #include "aidge/operator/Conv.hpp"
 #include "aidge/operator/ConvDepthWise.hpp"
 #include "aidge/operator/Pad.hpp"
+#include "aidge/operator/Memorize.hpp"
+#include "aidge/operator/Add.hpp"
+#include "aidge/operator/Mul.hpp"
+#include "aidge/operator/FC.hpp"
+#include "aidge/operator/Identity.hpp"
+#include "aidge/operator/Concat.hpp"
+#include "aidge/operator/Tanh.hpp"
+#include "aidge/operator/Sigmoid.hpp"
 
 namespace Aidge {
 template <std::array<DimSize_t, 1>::size_type DIM>
@@ -27,11 +35,12 @@ inline std::shared_ptr<Node> PaddedConv(DimSize_t in_channels,
                                   const std::string& name = "",
                                   const std::array<DimSize_t, DIM> &stride_dims = create_array<DimSize_t,DIM>(1),
                                   const std::array<DimSize_t, 2*DIM> &padding_dims = create_array<DimSize_t,2*DIM>(0),
-                                  const std::array<DimSize_t, DIM> &dilation_dims = create_array<DimSize_t,DIM>(1))
+                                  const std::array<DimSize_t, DIM> &dilation_dims = create_array<DimSize_t,DIM>(1),
+                                  bool no_bias = false)
 {
     // Construct micro-graph
     auto pad = Pad<DIM>(padding_dims, (!name.empty()) ? name + "_pad" : "", PadBorderType::Constant, 0.0);
-    auto conv = std::make_shared<Node>(std::make_shared<Conv_Op<static_cast<DimIdx_t>(DIM)>>(in_channels, out_channels, kernel_dims, stride_dims, dilation_dims), (!name.empty()) ? name + "_conv" : "");
+    auto conv = std::make_shared<Node>(std::make_shared<Conv_Op<static_cast<DimIdx_t>(DIM)>>(in_channels, out_channels, kernel_dims, stride_dims, dilation_dims, no_bias), (!name.empty()) ? name + "_conv" : "");
 
     auto metaOp = MetaOperator("PaddedConv", Sequential({pad, conv}), name);
     addProducer(metaOp, 1, append(out_channels, append(in_channels, kernel_dims)), "w");
@@ -48,9 +57,10 @@ inline std::shared_ptr<Node> PaddedConv(
     const std::string& name = "",
     const std::array<DimSize_t, DIM> &stride_dims = create_array<DimSize_t,DIM>(1),
     const std::array<DimSize_t, 2*DIM> &padding_dims = create_array<DimSize_t,2*DIM>(0),
-    const std::array<DimSize_t, DIM> &dilation_dims = create_array<DimSize_t,DIM>(1))
+    const std::array<DimSize_t, DIM> &dilation_dims = create_array<DimSize_t,DIM>(1),
+    bool no_bias = false)
 {
-    return PaddedConv(in_channels, out_channels, to_array(kernel_dims), name, stride_dims, padding_dims, dilation_dims);
+    return PaddedConv(in_channels, out_channels, to_array(kernel_dims), name, stride_dims, padding_dims, dilation_dims, no_bias);
 }
 
 template <std::array<DimSize_t, 1>::size_type DIM>
@@ -59,11 +69,12 @@ inline std::shared_ptr<Node> PaddedConvDepthWise(const DimSize_t nb_channels,
                                   const std::string& name = "",
                                   const std::array<DimSize_t, DIM> &stride_dims = create_array<DimSize_t,DIM>(1),
                                   const std::array<DimSize_t, 2*DIM> &padding_dims = create_array<DimSize_t,2*DIM>(0),
-                                  const std::array<DimSize_t, DIM> &dilation_dims = create_array<DimSize_t,DIM>(1))
+                                  const std::array<DimSize_t, DIM> &dilation_dims = create_array<DimSize_t,DIM>(1),
+                                  bool no_bias = false)
 {
     // Construct micro-graph
     auto pad = Pad<DIM>(padding_dims, (!name.empty()) ? name + "_pad" : "", PadBorderType::Constant, 0.0);
-    auto conv = std::make_shared<Node>(std::make_shared<ConvDepthWise_Op<static_cast<DimIdx_t>(DIM)>>(nb_channels, kernel_dims, stride_dims, dilation_dims), (!name.empty()) ? name + "_conv" : "");
+    auto conv = std::make_shared<Node>(std::make_shared<ConvDepthWise_Op<static_cast<DimIdx_t>(DIM)>>(nb_channels, kernel_dims, stride_dims, dilation_dims, no_bias), (!name.empty()) ? name + "_conv" : "");
 
     auto metaOp = MetaOperator("PaddedConvDepthWise", Sequential({pad, conv}), name);
     addProducer(metaOp, 1, append(nb_channels, append(DimSize_t(1), kernel_dims)), "w");
@@ -79,9 +90,10 @@ inline std::shared_ptr<Node> PaddedConvDepthWise(
     const std::string& name = "",
     const std::array<DimSize_t, DIM> &stride_dims = create_array<DimSize_t,DIM>(1),
     const std::array<DimSize_t, 2*DIM> &padding_dims = create_array<DimSize_t,2*DIM>(0),
-    const std::array<DimSize_t, DIM> &dilation_dims = create_array<DimSize_t,DIM>(1))
+    const std::array<DimSize_t, DIM> &dilation_dims = create_array<DimSize_t,DIM>(1),
+    bool no_bias = false)
 {
-    return PaddedConvDepthWise(nb_channels, to_array(kernel_dims), name, stride_dims, padding_dims, dilation_dims);
+    return PaddedConvDepthWise(nb_channels, to_array(kernel_dims), name, stride_dims, padding_dims, dilation_dims, no_bias);
 }
 
 template <std::array<DimSize_t, 1>::size_type DIM>
@@ -135,6 +147,116 @@ inline std::shared_ptr<Node> PaddedMaxPooling(
 {
     return PaddedMaxPooling(to_array(kernel_dims), name, stride_dims, padding_dims, ceil_mode);
 }
+
+inline std::shared_ptr<Node> LSTM(DimSize_t in_channels,
+                                  DimSize_t hidden_channels,
+                                  DimSize_t seq_length,
+                                  bool noBias = false,
+                                  const std::string& name = "")
+{
+    // Construct micro-graph
+    auto input = Identity((!name.empty()) ? name + "_input" : "");
+    auto hiddenState = Memorize(seq_length, (!name.empty()) ? name + "_hidden_state" : "");
+    auto cellState = Memorize(seq_length, (!name.empty()) ? name + "_cell_state" : "");
+    auto add = Add(2, (!name.empty()) ? name + "_add" : "");
+
+    // Forget gate
+    auto forgetGateX = std::make_shared<Node>(std::make_shared<FC_Op>(hidden_channels, noBias), (!name.empty()) ? name + "_forgetGateX" : "");
+    input->addChild(forgetGateX, 0, 0);
+    auto forgetGateH = std::make_shared<Node>(std::make_shared<FC_Op>(hidden_channels, noBias), (!name.empty()) ? name + "_forgetGateH" : "");
+    hiddenState->addChild(forgetGateH, 1, 0);
+    auto forgetGate = Add(2, (!name.empty()) ? name + "_forgetGate" : "");
+    forgetGateX->addChild(forgetGate, 0, 0);
+    forgetGateH->addChild(forgetGate, 0, 1);
+    auto forgetGateAct = Sigmoid((!name.empty()) ? name + "_forgetGateAct" : "");
+    auto forgetGateMul = Mul((!name.empty()) ? name + "_forgetGateMul" : "");
+    forgetGate->addChild(forgetGateAct, 0, 0);
+    forgetGateAct->addChild(forgetGateMul, 0, 0);
+    forgetGateMul->addChild(add, 0, 0);
+    cellState->addChild(forgetGateMul, 1, 1);
+
+    // Input gate
+    auto inputGateX = std::make_shared<Node>(std::make_shared<FC_Op>(hidden_channels, noBias), (!name.empty()) ? name + "_inputGateX" : "");
+    input->addChild(inputGateX, 0, 0);
+    auto inputGateH = std::make_shared<Node>(std::make_shared<FC_Op>(hidden_channels, noBias), (!name.empty()) ? name + "_inputGateH" : "");
+    hiddenState->addChild(inputGateH, 1, 0);
+    auto inputGate = Add(2, (!name.empty()) ? name + "_inputGate" : "");
+    inputGateX->addChild(inputGate, 0, 0);
+    inputGateH->addChild(inputGate, 0, 1);
+    auto inputGateAct = Sigmoid((!name.empty()) ? name + "_inputGateAct" : "");
+    auto inputGateMul = Mul((!name.empty()) ? name + "_inputGateMul" : "");
+    inputGate->addChild(inputGateAct, 0, 0);
+    inputGateAct->addChild(inputGateMul, 0, 0);
+    inputGateMul->addChild(add, 0, 1);
+
+    // Candidate for cell update
+    auto cellCandidateX = std::make_shared<Node>(std::make_shared<FC_Op>(hidden_channels, noBias), (!name.empty()) ? name + "_cellCandidateX" : "");
+    input->addChild(cellCandidateX, 0, 0);
+    auto cellCandidateH = std::make_shared<Node>(std::make_shared<FC_Op>(hidden_channels, noBias), (!name.empty()) ? name + "_cellCandidateH" : "");
+    hiddenState->addChild(cellCandidateH, 1, 0);
+    auto cellCandidate = Add(2, (!name.empty()) ? name + "_cellCandidate" : "");
+    cellCandidateX->addChild(cellCandidate, 0, 0);
+    cellCandidateH->addChild(cellCandidate, 0, 1);
+    auto cellCandidateAct = Tanh((!name.empty()) ? name + "_cellCandidateAct" : "");
+    cellCandidate->addChild(cellCandidateAct, 0, 0);
+    cellCandidateAct->addChild(inputGateMul, 0, 1);
+
+    // Output gate
+    auto outputGateX = std::make_shared<Node>(std::make_shared<FC_Op>(hidden_channels, noBias), (!name.empty()) ? name + "_outputGateX" : "");
+    input->addChild(outputGateX, 0, 0);
+    auto outputGateH = std::make_shared<Node>(std::make_shared<FC_Op>(hidden_channels, noBias), (!name.empty()) ? name + "_outputGateH" : "");
+    hiddenState->addChild(outputGateH, 1, 0);
+    auto outputGate = Add(2, (!name.empty()) ? name + "_outputGate" : "");
+    outputGateX->addChild(outputGate, 0, 0);
+    outputGateH->addChild(outputGate, 0, 1);
+    auto outputGateAct = Sigmoid((!name.empty()) ? name + "_outputGateAct" : "");
+    auto outputGateMul = Mul((!name.empty()) ? name + "_outputGateMul" : "");
+    outputGate->addChild(outputGateAct, 0, 0);
+    outputGateAct->addChild(outputGateMul, 0, 0);
+
+    // Updated cell state to help determine new hidden state
+    auto cellUpdatedAct = Tanh((!name.empty()) ? name + "_cellUpdatedAct" : "");
+    add->addChild(cellUpdatedAct, 0, 0);
+    cellUpdatedAct->addChild(outputGateMul, 0, 1);
+    outputGateMul->addChild(hiddenState, 0, 0);
+    add->addChild(cellState, 0, 0);
+
+    std::shared_ptr<GraphView> microGraph = std::make_shared<GraphView>();
+    microGraph->add(input);
+    microGraph->add({hiddenState, cellState, add,
+        forgetGateX, forgetGateH, forgetGate, forgetGateAct, forgetGateMul,
+        inputGateX, inputGateH, inputGate, inputGateAct, inputGateMul,
+        cellCandidateX, cellCandidateH, cellCandidate, cellCandidateAct,
+        outputGateX, outputGateH, outputGate, outputGateAct, outputGateMul,
+        cellUpdatedAct}, false);
+
+    microGraph->setOrderedInputs({{input, 0},
+        {inputGateX, 1}, {outputGateX, 1}, {forgetGateX, 1}, {cellCandidateX, 1},
+        {inputGateH, 1}, {outputGateH, 1}, {forgetGateH, 1}, {cellCandidateH, 1},
+        {inputGateX, 2}, {outputGateX, 2}, {forgetGateX, 2}, {cellCandidateX, 2},
+        {inputGateH, 2}, {outputGateH, 2}, {forgetGateH, 2}, {cellCandidateH, 2},
+        {hiddenState, 1}, {cellState, 1}});
+    microGraph->setOrderedOutputs({{hiddenState, 0}, {cellState, 0}});
+
+    auto metaOp = MetaOperator("LSTM", microGraph, name);
+    addProducer(metaOp, 1, {hidden_channels, in_channels}, "wi");
+    addProducer(metaOp, 2, {hidden_channels, in_channels}, "wo");
+    addProducer(metaOp, 3, {hidden_channels, in_channels}, "wf");
+    addProducer(metaOp, 4, {hidden_channels, in_channels}, "wc");
+    addProducer(metaOp, 5, {hidden_channels, hidden_channels}, "ri");
+    addProducer(metaOp, 6, {hidden_channels, hidden_channels}, "ro");
+    addProducer(metaOp, 7, {hidden_channels, hidden_channels}, "rf");
+    addProducer(metaOp, 8, {hidden_channels, hidden_channels}, "rc");
+    addProducer(metaOp, 9, {(noBias ? 0 : hidden_channels)}, "wbi");
+    addProducer(metaOp, 10, {(noBias ? 0 : hidden_channels)}, "wbo");
+    addProducer(metaOp, 11, {(noBias ? 0 : hidden_channels)}, "wbf");
+    addProducer(metaOp, 12, {(noBias ? 0 : hidden_channels)}, "wbc");
+    addProducer(metaOp, 13, {(noBias ? 0 : hidden_channels)}, "rbi");
+    addProducer(metaOp, 14, {(noBias ? 0 : hidden_channels)}, "rbo");
+    addProducer(metaOp, 15, {(noBias ? 0 : hidden_channels)}, "rbf");
+    addProducer(metaOp, 16, {(noBias ? 0 : hidden_channels)}, "rbc");
+    return metaOp;
+}
 }  // namespace Aidge
 
 #endif /* AIDGE_CORE_OPERATOR_METAOPERATORDEFS_H_ */
diff --git a/include/aidge/operator/Move.hpp b/include/aidge/operator/Move.hpp
index 62fb9897384673c695895b54557b4cf637aa2447..3652cf9697c6bcfea4befe4cdcdf5b9efff8b70c 100644
--- a/include/aidge/operator/Move.hpp
+++ b/include/aidge/operator/Move.hpp
@@ -72,4 +72,4 @@ inline std::shared_ptr<Node> Move(const std::string& name = "") {
 }
 }
 
-#endif /* AIDGE_CORE_OPERATOR_MOVE_H_ */
\ No newline at end of file
+#endif /* AIDGE_CORE_OPERATOR_MOVE_H_ */
diff --git a/include/aidge/operator/Mul.hpp b/include/aidge/operator/Mul.hpp
index 78b2fa5f98c9dae66ae291769f2de08d7805a738..cc9fba59431356a132330e453288f2f6e7141178 100644
--- a/include/aidge/operator/Mul.hpp
+++ b/include/aidge/operator/Mul.hpp
@@ -19,7 +19,6 @@
 #include "aidge/utils/Registrar.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/backend/OperatorImpl.hpp"
-#include "aidge/data/Tensor.hpp"
 #include "aidge/graph/Node.hpp"
 #include "aidge/utils/Types.h"
 
@@ -29,7 +28,7 @@ namespace Aidge {
  * @brief Tensor element-wise multiplication.
  */
 class Mul_Op : public OperatorTensor,
-    public Registrable<Mul_Op, std::string, std::unique_ptr<OperatorImpl>(const Mul_Op&)> {
+    public Registrable<Mul_Op, std::string, std::shared_ptr<OperatorImpl>(const Mul_Op&)> {
 public:
     static const std::string Type;
 
@@ -43,7 +42,11 @@ public:
     Mul_Op(const Mul_Op& op)
         : OperatorTensor(op)
     {
-        mImpl = op.mImpl ? Registrar<Mul_Op>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl) {
+            SET_IMPL_MACRO(Mul_Op, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -56,13 +59,10 @@ public:
 
     void computeOutputDims() override final;
 
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Mul_Op>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override;
 
     static const std::vector<std::string> getInputsName(){
-        return {"data_input"};
+        return {"data_input_1", "data_input_2"};
     }
     static const std::vector<std::string> getOutputsName(){
         return {"data_output"};
@@ -74,4 +74,4 @@ inline std::shared_ptr<Node> Mul(const std::string& name = "") {
 }
 } // namespace Aidge
 
-#endif /* AIDGE_CORE_OPERATOR_MUL_H_ */
\ No newline at end of file
+#endif /* AIDGE_CORE_OPERATOR_MUL_H_ */
diff --git a/include/aidge/operator/Operator.hpp b/include/aidge/operator/Operator.hpp
index cebc2d54041bb38c6e7f3434f12b559cec3d80af..3ee2342297208f6f4e4b061409bc5071c811d2ac 100644
--- a/include/aidge/operator/Operator.hpp
+++ b/include/aidge/operator/Operator.hpp
@@ -81,7 +81,7 @@ public:
     virtual void associateInput(const IOIndex_t inputIdx, const std::shared_ptr<Data>& data) = 0;
 
     /**
-     * @brief Set the specified input by performing a deep copy of the given data.
+     * @brief Set the specified input value by performing a deep copy of the given data.
      * The pointer itself is not changed, thus keeping the current connections.
      * @param inputIdx Index of the input to set.
      * @param data Data to copy.
@@ -90,7 +90,7 @@ public:
     virtual void setInput(const IOIndex_t inputIdx, std::shared_ptr<Data>&& data) = 0;
     virtual std::shared_ptr<Data> getRawInput(const IOIndex_t inputIdx) const = 0;
         /**
-     * @brief Set the specified output by performing a deep copy of the given data.
+     * @brief Set the specified output value by performing a deep copy of the given data.
      * The pointer itself is not changed, thus keeping the current connections.
      * @param inputIdx Index of the input to set.
      */
@@ -98,10 +98,10 @@ public:
     virtual void setOutput(const IOIndex_t outputIdx, std::shared_ptr<Data>&& data) = 0;
     virtual std::shared_ptr<Data> getRawOutput(const IOIndex_t outputIdx) const = 0;
 
-    std::shared_ptr<Hook> getHook(std::string hookName) {
+    std::shared_ptr<Hook> getHook(const std::string& hookName) {
         return mHooks[hookName];
     }
-    void addHook(std::string hookName) {
+    void addHook(const std::string& hookName) {
         mHooks.insert(std::pair<std::string, std::shared_ptr<Hook>>(hookName,Registrar<Hook>::create({hookName})(shared_from_this())));
     }
 
@@ -110,43 +110,60 @@ public:
 ///////////////////////////////////////////////////////
 //        IMPLEMENTATION
 ///////////////////////////////////////////////////////
+    std::string backend() const noexcept {
+        return mImpl ? mImpl->backend() : "";
+    }
 
     virtual void setBackend(const std::string& name, DeviceIdx_t device = 0) = 0;
     virtual void setDataType(const DataType& dataType) const = 0;
 
     /**
-     * @brief Set the a new OperatorImpl to the Operator
+     * @brief Set a new OperatorImpl to the Operator
+     *
+     */
+    inline void setImpl(std::shared_ptr<OperatorImpl> impl) { mImpl = impl; }
+
+    /**
+     * @brief Get the OperatorImpl of the Operator
      *
      */
-    void setImpl(std::shared_ptr<OperatorImpl> impl){
-        mImpl = impl;
+    inline std::shared_ptr<OperatorImpl> getImpl() const noexcept {
+        return mImpl;
     }
 
     /**
      * @brief Minimum amount of data from a specific input for one computation pass.
      * @param inputIdx Index of the input analysed.
-     * @return NbElts_t
+     * @return Elts_t
      */
-    virtual NbElts_t getNbRequiredData(const IOIndex_t inputIdx) const;
+    virtual Elts_t getNbRequiredData(const IOIndex_t inputIdx) const;
+
+    // Amount of input data that cannot be overwritten during the execution.
+    virtual Elts_t getNbRequiredProtected(const IOIndex_t inputIdx) const;
+
+    // Memory required at an output for a given input size.
+    virtual Elts_t getRequiredMemory(const IOIndex_t outputIdx, const std::vector<DimSize_t> &inputsSize) const;
 
     /**
-     * @brief Amount of data from a specific input actually used in one computation pass.
+     * @brief Total amount of consumed data from a specific input.
      *
      * @param inputIdx Index of the input analysed.
-     * @return NbElts_t
+     * @return Elts_t
      */
-    virtual NbElts_t getNbConsumedData(const IOIndex_t inputIdx) const;
+    virtual Elts_t getNbConsumedData(const IOIndex_t inputIdx) const;
 
     /**
-     * @brief Amount of data ready to be used on a specific output.
+     * @brief Total amount of produced data ready to be used on a specific output.
      *
      * @param outputIdx Index of the output analysed.
-     * @return NbElts_t
+     * @return Elts_t
      */
-    virtual NbElts_t getNbProducedData(const IOIndex_t outputIdx) const;
+    virtual Elts_t getNbProducedData(const IOIndex_t outputIdx) const;
 
     virtual void updateConsummerProducer();
 
+    virtual void resetConsummerProducer();
+
     virtual void forward();
 
     virtual void backward();
@@ -170,10 +187,10 @@ public:
     inline IOIndex_t nbParam() const noexcept { return mNbParam; };
     inline IOIndex_t nbOutputs() const noexcept { return mNbOut; };
 
-    static const std::vector<std::string> getInputsName(){
+    static const std::vector<std::string> getInputsName() {
         return {};
     }
-    static const std::vector<std::string> getOutputsName(){
+    static const std::vector<std::string> getOutputsName() {
         return {};
     }
 };
diff --git a/include/aidge/operator/OperatorTensor.hpp b/include/aidge/operator/OperatorTensor.hpp
index 504a416488651d43126a60981cd8afe0f95821f2..adf45c2d8311112fa145097ee98f46d120bd41ff 100644
--- a/include/aidge/operator/OperatorTensor.hpp
+++ b/include/aidge/operator/OperatorTensor.hpp
@@ -17,12 +17,12 @@
 #include <vector>
 
 #include "aidge/backend/OperatorImpl.hpp"
-#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Operator.hpp"
 #include "aidge/utils/Types.h"
 
 namespace Aidge {
 
+class Tensor;
 class OperatorTensor : public Operator {
     /* TODO: Add an attribute specifying the type of Data used by the Operator.
      * The same way ``Type`` attribute specifies the type of Operator. Hence this
@@ -41,26 +41,9 @@ public:
     OperatorTensor() = delete;
 
     OperatorTensor(const std::string& type, const IOIndex_t nbData, const IOIndex_t nbParam,
-                   const IOIndex_t nbOut)
-        : Operator(type, nbData, nbParam, nbOut, OperatorType::Tensor),
-          mInputs(std::vector<std::shared_ptr<Tensor>>(nbData + nbParam, nullptr)),
-          mOutputs(std::vector<std::shared_ptr<Tensor>>(nbOut)) {
-        for (std::size_t i = 0; i < static_cast<std::size_t>(nbOut); ++i) {
-            mOutputs[i] = std::make_shared<Tensor>();
-            mOutputs[i]->setDataType(DataType::Float32);
-        }
-    }
+                   const IOIndex_t nbOut);
 
-    OperatorTensor(const OperatorTensor& other)
-        : Operator(other),
-          mInputs(std::vector<std::shared_ptr<Tensor>>(other.nbInputs(), nullptr)),
-          mOutputs(std::vector<std::shared_ptr<Tensor>>(other.nbOutputs())) {
-        for (std::size_t i = 0; i < static_cast<std::size_t>(nbOutputs()); ++i) {
-            mOutputs[i] = std::make_shared<Tensor>();
-            // mOutputs[i] = std::make_shared<Tensor>(*(other.getOutput(i)));
-            // datatype already copied
-        }
-    }
+    OperatorTensor(const OperatorTensor& other);
 
     ~OperatorTensor();
 
@@ -76,17 +59,13 @@ public:
     void setInput(const IOIndex_t inputIdx, const std::shared_ptr<Data>& data) override final;
     void setInput(const IOIndex_t inputIdx, std::shared_ptr<Data>&& data) override final;
     const std::shared_ptr<Tensor>& getInput(const IOIndex_t inputIdx) const;
-    inline std::shared_ptr<Data> getRawInput(const IOIndex_t inputIdx) const override final {
-        return std::static_pointer_cast<Data>(getInput(inputIdx));
-    }
+    std::shared_ptr<Data> getRawInput(const IOIndex_t inputIdx) const override final;
 
     // output management
     void setOutput(const IOIndex_t outputIdx, const std::shared_ptr<Data>& data) override;
     void setOutput(const IOIndex_t outputIdx, std::shared_ptr<Data>&& data) override;
     virtual const std::shared_ptr<Tensor>& getOutput(const IOIndex_t outputIdx) const;
-    inline std::shared_ptr<Aidge::Data> getRawOutput(const Aidge::IOIndex_t outputIdx) const override final {
-        return std::static_pointer_cast<Data>(getOutput(outputIdx));
-    }
+    std::shared_ptr<Aidge::Data> getRawOutput(const Aidge::IOIndex_t outputIdx) const override final;
     ///////////////////////////////////////////////////
 
     ///////////////////////////////////////////////////
diff --git a/include/aidge/operator/Pad.hpp b/include/aidge/operator/Pad.hpp
index 56245dd2dfd62d4dc765de6e3d43b08c144cc62b..dce2a6e9e5ea9e0c5fe9a841c587c1f7bbe36fc7 100644
--- a/include/aidge/operator/Pad.hpp
+++ b/include/aidge/operator/Pad.hpp
@@ -31,7 +31,7 @@ enum class PadBorderType { Constant, Edge, Reflect, Wrap };
 
 template <DimIdx_t DIM>
 class Pad_Op : public OperatorTensor,
-                public Registrable<Pad_Op<DIM>, std::string, std::unique_ptr<OperatorImpl>(const Pad_Op<DIM> &)>,
+                public Registrable<Pad_Op<DIM>, std::string, std::shared_ptr<OperatorImpl>(const Pad_Op<DIM> &)>,
                 public StaticAttributes<PadAttr,
                                        std::array<DimSize_t, 2*DIM>,
                                        PadBorderType,
@@ -78,7 +78,7 @@ public:
         bool associated = true;
         for (IOIndex_t i = 0; i < nbInputs(); ++i) {
             if (!getInput(i)) {
-                AIDGE_THROW_OR_ABORT(std::runtime_error, "Every input should be associated with a Tensor");
+                AIDGE_THROW_OR_ABORT(std::runtime_error, "{}: input #{} should be associated with a Tensor", type(), i);
             }
             associated &= !(getInput(i)->empty());
         }
@@ -98,7 +98,7 @@ public:
     }
 
     void setBackend(const std::string &name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Pad_Op<DIM>>::create(name)(*this);
+        SET_IMPL_MACRO(Pad_Op<DIM>, *this, name);
         mOutputs[0]->setBackend(name, device);
     }
 
diff --git a/include/aidge/operator/Pop.hpp b/include/aidge/operator/Pop.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..9109ccaeb8bc648fe74510216fad93299740b9bf
--- /dev/null
+++ b/include/aidge/operator/Pop.hpp
@@ -0,0 +1,93 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CORE_OPERATOR_POP_H_
+#define AIDGE_CORE_OPERATOR_POP_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/graph/Node.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/StaticAttributes.hpp"
+#include "aidge/utils/Types.h"
+
+namespace Aidge {
+enum class PopAttr { ForwardStep };
+
+class Pop_Op : public OperatorTensor,
+    public Registrable<Pop_Op, std::string, std::unique_ptr<OperatorImpl>(const Pop_Op&)>,
+    public StaticAttributes<PopAttr, unsigned int> {
+public:
+    static const std::string Type;
+
+    using Attributes_ = StaticAttributes<PopAttr, unsigned int>;
+    template <PopAttr e>
+    using attr = typename Attributes_::template attr<e>;
+
+    Pop_Op()
+        : OperatorTensor(Type, 1, 0, 1),
+          Attributes_(attr<PopAttr::ForwardStep>(0))
+    {}
+
+    /**
+     * @brief Copy-constructor. Copy the operator attributes and its output tensor(s), but not its input tensors (the new operator has no input associated).
+     * @param op Operator to copy.
+     */
+    Pop_Op(const Pop_Op& op)
+        : OperatorTensor(op),
+          Attributes_(op)
+    {
+        if (op.mImpl){
+            SET_IMPL_MACRO(Pop_Op, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
+    }
+
+    /**
+     * @brief Clone the operator using its copy-constructor.
+     * @see Operator::Pop_Op
+     */
+    std::shared_ptr<Operator> clone() const override {
+        return std::make_shared<Pop_Op>(*this);
+    }
+
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override final;
+
+    void computeOutputDims() override final;
+    void updateConsummerProducer() override;
+    void forward() override;
+
+    static const std::vector<std::string> getInputsName(){
+        return {"data_input"};
+    }
+    static const std::vector<std::string> getOutputsName(){
+        return {"data_output"};
+    }
+};
+
+inline std::shared_ptr<Node> Pop(const std::string& name = "") {
+    return std::make_shared<Node>(std::make_shared<Pop_Op>(), name);
+}
+}  // namespace Aidge
+
+namespace {
+template <>
+const char *const EnumStrings<Aidge::PopAttr>::data[] = {
+    "ForwardStep"
+};
+}
+
+#endif /* AIDGE_CORE_OPERATOR_POP_H_ */
\ No newline at end of file
diff --git a/include/aidge/operator/Pow.hpp b/include/aidge/operator/Pow.hpp
index d498cacc7c5b2ddc3269f3ebc77707aead8eb52d..f2becdc60ceb44c19e341496f71e09f061cea55f 100644
--- a/include/aidge/operator/Pow.hpp
+++ b/include/aidge/operator/Pow.hpp
@@ -12,22 +12,20 @@
 #ifndef AIDGE_CORE_OPERATOR_POW_H_
 #define AIDGE_CORE_OPERATOR_POW_H_
 
-#include <cassert>
 #include <memory>
+#include <string>
 #include <vector>
 
 #include "aidge/utils/Registrar.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/backend/OperatorImpl.hpp"
-#include "aidge/data/Tensor.hpp"
-#include "aidge/data/Data.hpp"
 #include "aidge/graph/Node.hpp"
 #include "aidge/utils/Types.h"
 
 namespace Aidge {
 
 class Pow_Op : public OperatorTensor,
-    public Registrable<Pow_Op, std::string, std::unique_ptr<OperatorImpl>(const Pow_Op&)> {
+    public Registrable<Pow_Op, std::string, std::shared_ptr<OperatorImpl>(const Pow_Op&)> {
 public:
     static const std::string Type;
 
@@ -40,7 +38,11 @@ public:
     Pow_Op(const Pow_Op& op)
         : OperatorTensor(op)
     {
-        mImpl = op.mImpl ? Registrar<Pow_Op>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl){
+            SET_IMPL_MACRO(Pow_Op, *this, op.backend());
+        }else{
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -54,15 +56,12 @@ public:
     void computeOutputDims() override final;
 
 
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Pow_Op>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override final;
 
-    static const std::vector<std::string> getInputsName(){
-        return {"data_input"};
+    static const std::vector<std::string> getInputsName() {
+        return {"data_input_1", "data_input_2"};
     }
-    static const std::vector<std::string> getOutputsName(){
+    static const std::vector<std::string> getOutputsName() {
         return {"data_output"};
     }
 };
@@ -72,4 +71,4 @@ inline std::shared_ptr<Node> Pow(const std::string& name = "") {
 }
 } // namespace Aidge
 
-#endif /* AIDGE_CORE_OPERATOR_POW_H_ */
\ No newline at end of file
+#endif /* AIDGE_CORE_OPERATOR_POW_H_ */
diff --git a/include/aidge/operator/Producer.hpp b/include/aidge/operator/Producer.hpp
index fe9b044e2309eb7e724d6648b84c044d7407bafb..1e5a3940ba22c659121e76e1855353168d68441a 100644
--- a/include/aidge/operator/Producer.hpp
+++ b/include/aidge/operator/Producer.hpp
@@ -12,7 +12,9 @@
 #ifndef AIDGE_CORE_OPERATOR_PRODUCER_H_
 #define AIDGE_CORE_OPERATOR_PRODUCER_H_
 
+#include <cstddef>
 #include <array>
+#include <memory>
 #include <vector>
 
 #include "aidge/utils/Types.h"
@@ -28,7 +30,7 @@ enum class ProdAttr { Constant };
 
 class Producer_Op
     : public OperatorTensor,
-      public Registrable<Producer_Op, std::string, std::unique_ptr<OperatorImpl>(
+      public Registrable<Producer_Op, std::string, std::shared_ptr<OperatorImpl>(
                                           const Producer_Op &)>,
       public StaticAttributes<ProdAttr, bool> {
 public:
@@ -42,35 +44,39 @@ public:
     Producer_Op(const std::array<DimSize_t, DIM>& dims,
                 bool constant = false)
         : OperatorTensor(Type, 0, 0, 1),
-        Attributes_(attr<ProdAttr::Constant>(constant))
+          Attributes_(attr<ProdAttr::Constant>(constant))
     {
         mOutputs[0]->resize(dims);
+        mImpl = std::make_shared<OperatorImpl>(*this, "");
     }
 
-    Producer_Op(const std::shared_ptr<Tensor> tensor, bool constant = false)
-        : OperatorTensor(Type, 0, 0, 1),
-        Attributes_(attr<ProdAttr::Constant>(constant))
-    {
-        mOutputs[0] = tensor; // copy the pointer of the Tensor
-    }
+    /**
+     * @brief Construct a new Producer_Op object from a Tensor.
+     *
+     * @param tensor Tensor to set in the Prducer.
+     * @param constant Whether the Producer should be considered constant.
+     */
+    Producer_Op(const std::shared_ptr<Tensor> tensor, bool constant = false);
 
     /**
-     * @brief Copy-constructor. Copy the operator attributes and its output tensor(s), but not its input tensors (the new operator has no input associated).
+     * @brief Copy-constructor. Copy the operator attributes and its output tensor(s),
+     * but not its input tensors (the new operator has no input associated).
      * @param op OperatorTensor to copy.
      */
-    Producer_Op(const Producer_Op& op)
-        : OperatorTensor(op),
-          Attributes_(op)
-    {
-        for (std::size_t i = 0; i < static_cast<std::size_t>(nbOutputs()); ++i) {
-            mOutputs[i] = std::make_shared<Tensor>(*(op.getOutput(i)));
-        }
-        mImpl = op.mImpl ? Registrar<Producer_Op>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
-    }
+    Producer_Op(const Producer_Op& op);
 
+public:
+    /**
+     * @brief Conversion operator from Producer to Tensor.
+     *
+     * @return std::shared_ptr<Tensor>
+     */
+    operator std::shared_ptr<Tensor>() const { return mOutputs[0]; }
+
+public:
     /**
      * @brief Clone the operator using its copy-constructor.
-     * @see Operator::Producer_Op
+     * @see Operator::Producer_Op(const Producer_Op&)
      */
     std::shared_ptr<Operator> clone() const override {
         return std::make_shared<Producer_Op>(*this);
@@ -80,17 +86,14 @@ public:
         AIDGE_THROW_OR_ABORT(std::runtime_error, "Producer operator takes no input.");
     }
 
-    void computeOutputDims() override final {}
+    void computeOutputDims() noexcept override final {}
 
-    bool outputDimsForwarded() const override final {return true;}
+    inline bool outputDimsForwarded() const noexcept override final { return true; }
 
 
     inline const std::vector<DimSize_t> dims() const noexcept { return mOutputs[0]->dims(); }
 
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Producer_Op>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override;
 
     static const std::vector<std::string> getInputsName(){
         return {};
@@ -99,12 +102,11 @@ public:
         return {"data_output"};
     }
 
-public:
     void forward() override final {
-        printf("Basic Producer forward() function.\n");
+        fmt::print("Basic Producer forward() function.\n");
     }
     void backward() override final {
-        printf("Basic Producer backward() function.\n");
+        fmt::print("Basic Producer backward() function.\n");
     }
     void setOutput(const Aidge::IOIndex_t outputIdx, std::shared_ptr<Aidge::Data>&& data) override {
         if (getAttr<ProdAttr::Constant>()) {
diff --git a/include/aidge/operator/ReLU.hpp b/include/aidge/operator/ReLU.hpp
index 0bb7cdffe421b973ae7c86b4569e7464b3cf6da4..963de31c49f48784e92434b2b563d6c008e2d4fd 100644
--- a/include/aidge/operator/ReLU.hpp
+++ b/include/aidge/operator/ReLU.hpp
@@ -16,17 +16,17 @@
 #include <memory>
 #include <vector>
 
-#include "aidge/utils/Registrar.hpp"
-#include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/backend/OperatorImpl.hpp"
-#include "aidge/data/Tensor.hpp"
 #include "aidge/graph/Node.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Registrar.hpp"
 #include "aidge/utils/Types.h"
 
 namespace Aidge {
 
 class ReLU_Op : public OperatorTensor,
-    public Registrable<ReLU_Op, std::string, std::unique_ptr<OperatorImpl>(const ReLU_Op&)> {
+    public Registrable<ReLU_Op, std::string, std::shared_ptr<OperatorImpl>(const ReLU_Op&)> {
 public:
     static const std::string Type;
 
@@ -39,7 +39,11 @@ public:
     ReLU_Op(const ReLU_Op& op)
         : OperatorTensor(op)
     {
-        mImpl = op.mImpl ? Registrar<ReLU_Op>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl){
+            SET_IMPL_MACRO(ReLU_Op, *this, op.backend());
+        }else{
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -51,10 +55,7 @@ public:
     }
 
 
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<ReLU_Op>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override final;
 
     static const std::vector<std::string> getInputsName(){
         return {"data_input"};
@@ -69,4 +70,4 @@ inline std::shared_ptr<Node> ReLU(const std::string& name = "") {
 }
 }
 
-#endif /* AIDGE_CORE_OPERATOR_RELU_H_ */
\ No newline at end of file
+#endif /* AIDGE_CORE_OPERATOR_RELU_H_ */
diff --git a/include/aidge/operator/ReduceMean.hpp b/include/aidge/operator/ReduceMean.hpp
index 52d0118743373c23a4afe4a51d3f22adbe9e6848..ab27e4e0233052f7cc155ed0375175a27d3edcf5 100644
--- a/include/aidge/operator/ReduceMean.hpp
+++ b/include/aidge/operator/ReduceMean.hpp
@@ -12,15 +12,15 @@
 #ifndef AIDGE_CORE_OPERATOR_REDUCEMEAN_H_
 #define AIDGE_CORE_OPERATOR_REDUCEMEAN_H_
 
-#include <array>
-#include <cmath>
-#include <numeric>
+#include <cstdint>    // std::int32_t
+#include <memory>
+#include <string>
 #include <vector>
 
-#include "aidge/data/Tensor.hpp"
 #include "aidge/graph/Node.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/operator/Producer.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
 #include "aidge/utils/StaticAttributes.hpp"
 #include "aidge/utils/Registrar.hpp"
 #include "aidge/utils/Types.h"
@@ -28,21 +28,20 @@
 namespace Aidge {
 enum class ReduceMeanAttr { Axes, KeepDims };
 
-template <DimIdx_t DIM>
 class ReduceMean_Op : public OperatorTensor,
-                public Registrable<ReduceMean_Op<DIM>, std::string, std::unique_ptr<OperatorImpl>(const ReduceMean_Op<DIM> &)>,
-                public StaticAttributes<ReduceMeanAttr, std::array<int, DIM>, DimSize_t> {
+                public Registrable<ReduceMean_Op, std::string, std::shared_ptr<OperatorImpl>(const ReduceMean_Op &)>,
+                public StaticAttributes<ReduceMeanAttr, std::vector<std::int32_t>, DimSize_t> {
 
    public:
     static const std::string Type;
 
     ReduceMean_Op() = delete;
 
-    using Attributes_ = StaticAttributes<ReduceMeanAttr, std::array<int, DIM>, DimSize_t>;
+    using Attributes_ = StaticAttributes<ReduceMeanAttr, std::vector<std::int32_t>, DimSize_t>;
     template <ReduceMeanAttr e>
     using attr = typename Attributes_::template attr<e>;
 
-    constexpr ReduceMean_Op(const std::array<int, DIM> &axes, DimSize_t keep_dims)
+    ReduceMean_Op(const std::vector<std::int32_t>& axes, DimSize_t keep_dims)
         : OperatorTensor(Type, 1, 0, 1),
           Attributes_(attr<ReduceMeanAttr::Axes>(axes),
                       attr<ReduceMeanAttr::KeepDims>(keep_dims)) {}
@@ -51,11 +50,15 @@ class ReduceMean_Op : public OperatorTensor,
      * @brief Copy-constructor. Copy the operator attributes and its output tensor(s), but not its input tensors (the new operator has no input associated).
      * @param op Operator to copy.
      */
-    ReduceMean_Op(const ReduceMean_Op<DIM>& op)
+    ReduceMean_Op(const ReduceMean_Op& op)
         : OperatorTensor(op),
           Attributes_(op)
     {
-        mImpl = op.mImpl ? Registrar<ReduceMean_Op<DIM>>::create(mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl){
+            SET_IMPL_MACRO(ReduceMean_Op, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -63,75 +66,51 @@ class ReduceMean_Op : public OperatorTensor,
      * @see Operator::ReduceMean_Op
      */
     std::shared_ptr<Operator> clone() const override {
-        return std::make_shared<ReduceMean_Op<DIM>>(*this);
+        return std::make_shared<ReduceMean_Op>(*this);
     }
 
-    void computeOutputDims() override final {
-        if (!getInput(0)->empty()) {
-            std::vector<DimSize_t> outDims;
-            for(std::size_t d=0; d<getInput(0)->dims().size(); ++d)
-            {
-                bool reducedDim =  false;
-                for(std::size_t i=0; i<DIM; ++i)
-                {
-                    int axis_ = this->template getAttr<ReduceMeanAttr::Axes>()[i];
-                    std::size_t axis= axis_>=0? axis_: axis_ + getInput(0)->nbDims();
-                    if(axis == d)
-                    {
-                        reducedDim = true;
-                        break;
-                    }
-                }
-                if(reducedDim)
-                {
-                    if(this->template getAttr<ReduceMeanAttr::KeepDims>())
-                        outDims.push_back(1);
-                }
-                else
-                    outDims.push_back(getInput(0)->dims()[d]);
-            }
-            if(outDims.size()>0)
-                mOutputs[0]->resize(outDims);
-            else
-                mOutputs[0]->resize({1});
-        }
-    }
+    void computeOutputDims() override final;
 
-    void setBackend(const std::string &name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<ReduceMean_Op<DIM>>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string &name, DeviceIdx_t device = 0) override final;
 
-    static const std::vector<std::string> getInputsName(){
+    static const std::vector<std::string> getInputsName() {
         return {"data_input"};
     }
-    static const std::vector<std::string> getOutputsName(){
+    static const std::vector<std::string> getOutputsName() {
         return {"data_output"};
     }
 };
 
-template <std::array<DimSize_t, 1>::size_type DIM>
-inline std::shared_ptr<Node> ReduceMean(const std::array<int, DIM> &axes,
+/**
+ * @brief Compute the mean value of a Tensor over the provided axes. Dimensions
+ * may be reduced by erasing the provided axes or not.
+ *
+ * @param axes Dimensions over which data mean should be computed.
+ * @param keep_dims Whether or not reduced dimensions are to be erased.
+ * @param name Name of the Operator.
+ * @return std::shared_ptr<Node> Node containing the Operator.
+ */
+inline std::shared_ptr<Node> ReduceMean(const std::vector<std::int32_t> &axes,
                                         DimSize_t keep_dims=1,
                                         const std::string& name = "") {
     // FIXME: properly handle default w&b initialization in every cases
-    static_assert(DIM<=MaxDim,"Too many kernel dimensions required by ReduceMean, not supported");
-    return std::make_shared<Node>(std::make_shared<ReduceMean_Op<static_cast<DimIdx_t>(DIM)>>(axes, keep_dims), name);
+    AIDGE_ASSERT(axes.size()<=MaxDim, "Too many kernel dimensions required by ReduceMean, not supported");
+    return std::make_shared<Node>(std::make_shared<ReduceMean_Op>(axes, keep_dims), name);
 
 }
 
 // helper with C-style array instead of std::array for kernel_dims to allow automatic template DIM deduction
-template <DimSize_t DIM>
-inline std::shared_ptr<Node> ReduceMean(
-    int const (&axes)[DIM],
-    DimSize_t keep_dims = 1,
-    const std::string& name = "") {
-    static_assert(DIM<=MaxDim,"Too many kernel dimensions required by ReduceMean, not supported");
-    return ReduceMean(to_array(axes), keep_dims, name);
-}
-
-template <DimIdx_t DIM>
-const std::string ReduceMean_Op<DIM>::Type = "ReduceMean";
+// template <DimSize_t DIM>
+// inline std::shared_ptr<Node> ReduceMean(
+//     std::int32_t const (&axes)[DIM],
+//     DimSize_t keep_dims = 1,
+//     const std::string& name = "") {
+//     static_assert(DIM<=MaxDim,"Too many kernel dimensions required by ReduceMean, not supported");
+//     return ReduceMean(to_array(axes), keep_dims, name);
+// }
+
+// template <DimIdx_t DIM>
+// const std::string ReduceMean_Op::Type = "ReduceMean";
 
 }  // namespace Aidge
 
diff --git a/include/aidge/operator/Reshape.hpp b/include/aidge/operator/Reshape.hpp
index 32d71d5adc3cfd92c9840dcb5bc61bfb6399c6db..060029bb87ea142728056b3817b8162d566cb458 100644
--- a/include/aidge/operator/Reshape.hpp
+++ b/include/aidge/operator/Reshape.hpp
@@ -12,7 +12,6 @@
 #ifndef AIDGE_CORE_OPERATOR_RESHAPE_H_
 #define AIDGE_CORE_OPERATOR_RESHAPE_H_
 
-#include <cassert>
 #include <memory>
 #include <vector>
 
@@ -28,7 +27,7 @@ namespace Aidge {
 enum class ReshapeAttr { Shape };
 
 class Reshape_Op : public OperatorTensor,
-                   public Registrable<Reshape_Op, std::string, std::unique_ptr<OperatorImpl>(const Reshape_Op&)>,
+                   public Registrable<Reshape_Op, std::string, std::shared_ptr<OperatorImpl>(const Reshape_Op&)>,
                    public StaticAttributes<ReshapeAttr, std::vector<std::int64_t>> {
 
 public:
@@ -53,7 +52,11 @@ public:
         : OperatorTensor(op),
           Attributes_(op)
     {
-        mImpl = op.mImpl ? Registrar<Reshape_Op>::create(mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl){
+            SET_IMPL_MACRO(Reshape_Op, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -66,10 +69,7 @@ public:
 
     void computeOutputDims() override final;
 
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Reshape_Op>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override final;
 
     static const std::vector<std::string> getInputsName(){
         return {"data_input"};
diff --git a/include/aidge/operator/Scaling.hpp b/include/aidge/operator/Scaling.hpp
index 54f1d98d2f61d18dd821c9f0a6b574bb52b0c9f0..8f54ab217631ac69a4e16555f8e58f550ab0156c 100644
--- a/include/aidge/operator/Scaling.hpp
+++ b/include/aidge/operator/Scaling.hpp
@@ -9,18 +9,17 @@
  *
  ********************************************************************************/
 
-#ifndef __AIDGE_CORE_OPERATOR_Scaling_H__
-#define __AIDGE_CORE_OPERATOR_Scaling_H__
+#ifndef AIDGE_CORE_OPERATOR_SCALING_H_
+#define AIDGE_CORE_OPERATOR_SCALING_H_
 
 #include <vector>
 #include <memory>
 
-#include "aidge/utils/StaticAttributes.hpp"
-#include "aidge/utils/Registrar.hpp"
-#include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/backend/OperatorImpl.hpp"
-#include "aidge/data/Tensor.hpp"
 #include "aidge/graph/Node.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/utils/StaticAttributes.hpp"
+#include "aidge/utils/Registrar.hpp"
 #include "aidge/utils/Types.h"
 
 namespace Aidge {
@@ -55,7 +54,11 @@ public:
         : OperatorTensor(op),
           Attributes_(op)
     {
-        mImpl = op.mImpl ? Registrar<Scaling_Op>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl){
+            SET_IMPL_MACRO(Scaling_Op, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -66,10 +69,7 @@ public:
         return std::make_shared<Scaling_Op>(*this);
     }
 
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Scaling_Op>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override final;
 
     static const std::vector<std::string> getInputsName() {
         return {"data_input"};
@@ -95,4 +95,4 @@ const char* const EnumStrings<Aidge::ScalingAttr>::data[]
     = {"scalingFactor", "quantizedNbBits", "isOutputUnsigned"};
 }
 
-#endif /* __AIDGE_CORE_OPERATOR_RELU_H__ */
\ No newline at end of file
+#endif /* AIDGE_CORE_OPERATOR_SCALING_H_ */
diff --git a/include/aidge/operator/Sigmoid.hpp b/include/aidge/operator/Sigmoid.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..bea9fc45eaa7f17f71963106b5bd3e1340a48a92
--- /dev/null
+++ b/include/aidge/operator/Sigmoid.hpp
@@ -0,0 +1,73 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CORE_OPERATOR_SIGMOID_H_
+#define AIDGE_CORE_OPERATOR_SIGMOID_H_
+
+#include <cassert>
+#include <memory>
+#include <vector>
+
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/graph/Node.hpp"
+#include "aidge/utils/Types.h"
+
+namespace Aidge {
+
+class Sigmoid_Op : public OperatorTensor,
+    public Registrable<Sigmoid_Op, std::string, std::unique_ptr<OperatorImpl>(const Sigmoid_Op&)> {
+public:
+    static const std::string Type;
+
+    Sigmoid_Op() : OperatorTensor(Type, 1, 0, 1) {}
+
+    /**
+     * @brief Copy-constructor. Copy the operator attributes and its output tensor(s), but not its input tensors (the new operator has no input associated).
+     * @param op Operator to copy.
+     */
+    Sigmoid_Op(const Sigmoid_Op& op)
+        : OperatorTensor(op)
+    {
+        if (op.mImpl){
+            SET_IMPL_MACRO(Sigmoid_Op, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
+    }
+
+    /**
+     * @brief Clone the operator using its copy-constructor.
+     * @see Operator::Sigmoid_Op
+     */
+    std::shared_ptr<Operator> clone() const override {
+        return std::make_shared<Sigmoid_Op>(*this);
+    }
+
+
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override final;
+
+    static const std::vector<std::string> getInputsName(){
+        return {"data_input"};
+    }
+    static const std::vector<std::string> getOutputsName(){
+        return {"data_output"};
+    }
+};
+
+inline std::shared_ptr<Node> Sigmoid(const std::string& name = "") {
+    return std::make_shared<Node>(std::make_shared<Sigmoid_Op>(), name);
+}
+}
+
+#endif /* AIDGE_CORE_OPERATOR_SIGMOID_H_ */
\ No newline at end of file
diff --git a/include/aidge/operator/Slice.hpp b/include/aidge/operator/Slice.hpp
index 12a7425f3339b7fbc0ae010639aacf23d97b0f5f..f68aa17f480038d8ff7850577c438cfdc6704d59 100644
--- a/include/aidge/operator/Slice.hpp
+++ b/include/aidge/operator/Slice.hpp
@@ -28,18 +28,18 @@ enum class SliceAttr { Starts, Ends, Axes };
 
 class Slice_Op
     : public OperatorTensor,
-      public Registrable<Slice_Op, std::string, std::unique_ptr<OperatorImpl>(const Slice_Op &)>,
-      public StaticAttributes<SliceAttr, std::vector<std::int32_t>, std::vector<std::int32_t>, std::vector<std::int32_t>> {
+      public Registrable<Slice_Op, std::string, std::shared_ptr<OperatorImpl>(const Slice_Op &)>,
+      public StaticAttributes<SliceAttr, std::vector<std::int64_t>, std::vector<std::int64_t>, std::vector<std::int64_t>> {
 public:
     static const std::string Type;
 
     Slice_Op() = delete;
 
-    using Attributes_ = StaticAttributes<SliceAttr, std::vector<std::int32_t>, std::vector<std::int32_t>, std::vector<std::int32_t>>;
+    using Attributes_ = StaticAttributes<SliceAttr, std::vector<std::int64_t>, std::vector<std::int64_t>, std::vector<std::int64_t>>;
     template <SliceAttr e>
     using attr = typename Attributes_::template attr<e>;
 
-    Slice_Op(const std::vector<std::int32_t>& starts, const std::vector<std::int32_t>&  ends, const std::vector<std::int32_t>& axes)
+    Slice_Op(const std::vector<std::int64_t>& starts, const std::vector<std::int64_t>&  ends, const std::vector<std::int64_t>& axes)
         : OperatorTensor(Type, 1, 0, 1),
           Attributes_(attr<SliceAttr::Starts>(starts),
                       attr<SliceAttr::Ends>(ends),
@@ -55,8 +55,11 @@ public:
         : OperatorTensor(op),
           Attributes_(op)
     {
-        mImpl = op.mImpl ? Registrar<Slice_Op>::create(op.mOutputs[0]->getImpl()->backend())(*this)
-                         : nullptr;
+        if (op.mImpl){
+            SET_IMPL_MACRO(Slice_Op, *this, op.backend());
+        }else{
+            mImpl = nullptr;
+        }
     }
 
 public:
@@ -69,7 +72,7 @@ public:
     void computeOutputDims() override final;
 
     void setBackend(const std::string &name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Slice_Op>::create(name)(*this);
+        SET_IMPL_MACRO(Slice_Op, *this, name);
         mOutputs[0]->setBackend(name, device);
     }
 
@@ -94,9 +97,9 @@ public:
  * @param name Name of the Operator.
  * @return std::shared_ptr<Node> A Node containing the Operator.
  */
-inline std::shared_ptr<Node> Slice(const std::vector<std::int32_t> starts,
-                                   const std::vector<std::int32_t> ends,
-                                   const std::vector<std::int32_t> axes,
+inline std::shared_ptr<Node> Slice(const std::vector<std::int64_t> starts,
+                                   const std::vector<std::int64_t> ends,
+                                   const std::vector<std::int64_t> axes,
                                    const std::string &name = "") {
     // FIXME: properly handle default w&b initialization in every cases
     return std::make_shared<Node>(std::make_shared<Slice_Op>(starts, ends, axes), name);
diff --git a/include/aidge/operator/Softmax.hpp b/include/aidge/operator/Softmax.hpp
index ed6689dc97ef17276df260cd90649f2a75b10007..d48dbc2b60e46eb5c074b8adae065383e29b1769 100644
--- a/include/aidge/operator/Softmax.hpp
+++ b/include/aidge/operator/Softmax.hpp
@@ -12,14 +12,10 @@
 #ifndef AIDGE_CORE_OPERATOR_SOFTMAX_H_
 #define AIDGE_CORE_OPERATOR_SOFTMAX_H_
 
-#include <cassert>
 #include <memory>
 #include <vector>
 
-
 #include "aidge/backend/OperatorImpl.hpp"
-#include "aidge/data/Tensor.hpp"
-#include "aidge/data/Data.hpp"
 #include "aidge/graph/Node.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/operator/Producer.hpp"
@@ -33,7 +29,7 @@ enum class SoftmaxAttr { AxisIdx };
 class Softmax_Op : public OperatorTensor,
                 public Registrable<Softmax_Op,
                                    std::string,
-                                   std::unique_ptr<OperatorImpl>(const Softmax_Op&)>,
+                                   std::shared_ptr<OperatorImpl>(const Softmax_Op&)>,
                 public StaticAttributes<SoftmaxAttr, int> {
 
 public:
@@ -55,7 +51,11 @@ public:
         : OperatorTensor(op),
           Attributes_(op)
     {
-        mImpl = op.mImpl ? Registrar<Softmax_Op>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl){
+            SET_IMPL_MACRO(Softmax_Op, *this, op.backend());
+        }else{
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -66,10 +66,7 @@ public:
         return std::make_shared<Softmax_Op>(*this);
     }
 
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Softmax_Op>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override final;
 
     static const std::vector<std::string> getInputsName(){
         return {"data_input"};
diff --git a/include/aidge/operator/Sqrt.hpp b/include/aidge/operator/Sqrt.hpp
index 32adfdb93db1e9da857f4147efdcfe64bbb34475..f5ffa431192d73a703c1ce973cb485dadb31420d 100644
--- a/include/aidge/operator/Sqrt.hpp
+++ b/include/aidge/operator/Sqrt.hpp
@@ -12,22 +12,19 @@
 #ifndef AIDGE_CORE_OPERATOR_SQRT_H_
 #define AIDGE_CORE_OPERATOR_SQRT_H_
 
-#include <cassert>
 #include <memory>
 #include <vector>
 
-#include "aidge/utils/Registrar.hpp"
-#include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/backend/OperatorImpl.hpp"
-#include "aidge/data/Tensor.hpp"
-#include "aidge/data/Data.hpp"
 #include "aidge/graph/Node.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/utils/Registrar.hpp"
 #include "aidge/utils/Types.h"
 
 namespace Aidge {
 
 class Sqrt_Op : public OperatorTensor,
-    public Registrable<Sqrt_Op, std::string, std::unique_ptr<OperatorImpl>(const Sqrt_Op&)> {
+    public Registrable<Sqrt_Op, std::string, std::shared_ptr<OperatorImpl>(const Sqrt_Op&)> {
 public:
     // FIXME: change accessibility
     std::shared_ptr<Tensor> mInput = std::make_shared<Tensor>();
@@ -45,7 +42,11 @@ public:
     Sqrt_Op(const Sqrt_Op& op)
         : OperatorTensor(op)
     {
-        mImpl = op.mImpl ? Registrar<Sqrt_Op>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl){
+            SET_IMPL_MACRO(Sqrt_Op, *this, op.backend());
+        }else{
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -56,10 +57,7 @@ public:
         return std::make_shared<Sqrt_Op>(*this);
     }
 
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Sqrt_Op>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override final;
 
     static const std::vector<std::string> getInputsName(){
         return {"data_input"};
diff --git a/include/aidge/operator/Sub.hpp b/include/aidge/operator/Sub.hpp
index ee5efa24dc24ebcd5ad4c45491c968caf691eee9..fbcebcc9f62c23e9c60b5dff6f0d41c10d8b8717 100644
--- a/include/aidge/operator/Sub.hpp
+++ b/include/aidge/operator/Sub.hpp
@@ -12,22 +12,19 @@
 #ifndef AIDGE_CORE_OPERATOR_SUB_H_
 #define AIDGE_CORE_OPERATOR_SUB_H_
 
-#include <cassert>
 #include <memory>
 #include <vector>
 
-#include "aidge/utils/Registrar.hpp"
-#include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/backend/OperatorImpl.hpp"
-#include "aidge/data/Tensor.hpp"
-#include "aidge/data/Data.hpp"
 #include "aidge/graph/Node.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/utils/Registrar.hpp"
 #include "aidge/utils/Types.h"
 
 namespace Aidge {
 
 class Sub_Op : public OperatorTensor,
-    public Registrable<Sub_Op, std::string, std::unique_ptr<OperatorImpl>(const Sub_Op&)> {
+    public Registrable<Sub_Op, std::string, std::shared_ptr<OperatorImpl>(const Sub_Op&)> {
 public:
     // FIXME: change accessibility
     std::array<std::shared_ptr<Tensor>, 2> mInputs = {std::make_shared<Tensor>(), std::make_shared<Tensor>()};
@@ -45,7 +42,11 @@ public:
     Sub_Op(const Sub_Op& op)
         : OperatorTensor(op)
     {
-        mImpl = op.mImpl ? Registrar<Sub_Op>::create(op.mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl){
+            SET_IMPL_MACRO(Sub_Op, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -59,13 +60,10 @@ public:
     void computeOutputDims() override final;
 
 
-    void setBackend(const std::string& name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Sub_Op>::create(name)(*this);
-        mOutputs[0]->setBackend(name, device);
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override final;
 
     static const std::vector<std::string> getInputsName(){
-        return {"data_input"};
+        return {"data_input_1", "data_input_2"};
     }
     static const std::vector<std::string> getOutputsName(){
         return {"data_output"};
@@ -77,4 +75,4 @@ inline std::shared_ptr<Node> Sub(const std::string& name = "") {
 }
 } // namespace Aidge
 
-#endif /* AIDGE_CORE_OPERATOR_SUB_H_ */
\ No newline at end of file
+#endif /* AIDGE_CORE_OPERATOR_SUB_H_ */
diff --git a/include/aidge/operator/Tanh.hpp b/include/aidge/operator/Tanh.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..3fd5377d30cfff864743dcab2da9e690e26e5263
--- /dev/null
+++ b/include/aidge/operator/Tanh.hpp
@@ -0,0 +1,71 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CORE_OPERATOR_TANH_H_
+#define AIDGE_CORE_OPERATOR_TANH_H_
+
+#include <memory>
+#include <vector>
+
+#include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/graph/Node.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
+
+namespace Aidge {
+
+class Tanh_Op : public OperatorTensor,
+    public Registrable<Tanh_Op, std::string, std::unique_ptr<OperatorImpl>(const Tanh_Op&)> {
+public:
+    static const std::string Type;
+
+    Tanh_Op() : OperatorTensor(Type, 1, 0, 1) {}
+
+    /**
+     * @brief Copy-constructor. Copy the operator attributes and its output tensor(s), but not its input tensors (the new operator has no input associated).
+     * @param op Operator to copy.
+     */
+    Tanh_Op(const Tanh_Op& op)
+        : OperatorTensor(op)
+    {
+       if (op.mImpl){
+            SET_IMPL_MACRO(Tanh_Op, *this, op.backend());
+        } else {
+            mImpl = nullptr;
+        }
+    }
+
+    /**
+     * @brief Clone the operator using its copy-constructor.
+     * @see Operator::Tanh_Op
+     */
+    std::shared_ptr<Operator> clone() const override {
+        return std::make_shared<Tanh_Op>(*this);
+    }
+
+
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override final;
+
+    static const std::vector<std::string> getInputsName(){
+        return {"data_input"};
+    }
+    static const std::vector<std::string> getOutputsName(){
+        return {"data_output"};
+    }
+};
+
+inline std::shared_ptr<Node> Tanh(const std::string& name = "") {
+    return std::make_shared<Node>(std::make_shared<Tanh_Op>(), name);
+}
+}
+
+#endif /* AIDGE_CORE_OPERATOR_TANH_H_ */
\ No newline at end of file
diff --git a/include/aidge/operator/Transpose.hpp b/include/aidge/operator/Transpose.hpp
index 2262bec14bd2f00cda643ade0709f7f9d509fa22..1beb5781b9262669cd2acb6ce4ef3aae85843573 100644
--- a/include/aidge/operator/Transpose.hpp
+++ b/include/aidge/operator/Transpose.hpp
@@ -30,7 +30,7 @@ enum class TransposeAttr { OutputDimsOrder };
 
 template <DimIdx_t DIM>
 class Transpose_Op : public OperatorTensor,
-                public Registrable<Transpose_Op<DIM>, std::string, std::unique_ptr<OperatorImpl>(const Transpose_Op<DIM> &)>,
+                public Registrable<Transpose_Op<DIM>, std::string, std::shared_ptr<OperatorImpl>(const Transpose_Op<DIM> &)>,
                 public StaticAttributes<TransposeAttr,
                                        std::array<DimSize_t, DIM>> {
 
@@ -56,7 +56,11 @@ class Transpose_Op : public OperatorTensor,
         : OperatorTensor(op),
           Attributes_(op)
     {
-        mImpl = op.mImpl ? Registrar<Transpose_Op<DIM>>::create(mOutputs[0]->getImpl()->backend())(*this) : nullptr;
+        if (op.mImpl){
+            SET_IMPL_MACRO(Transpose_Op<DIM>, *this, op.backend());
+        }else{
+            mImpl = nullptr;
+        }
     }
 
     /**
@@ -80,7 +84,7 @@ class Transpose_Op : public OperatorTensor,
     }
 
     void setBackend(const std::string &name, DeviceIdx_t device = 0) override {
-        mImpl = Registrar<Transpose_Op<DIM>>::create(name)(*this);
+        SET_IMPL_MACRO(Transpose_Op<DIM>, *this, name);
         mOutputs[0]->setBackend(name, device);
     }
 
diff --git a/include/aidge/recipes/GraphViewHelper.hpp b/include/aidge/recipes/GraphViewHelper.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a2c571bf4ed164729f7c3416c814b913b4d07e6f
--- /dev/null
+++ b/include/aidge/recipes/GraphViewHelper.hpp
@@ -0,0 +1,46 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CORE_UTILS_GRAPHVIEWHELPER_H_
+#define AIDGE_CORE_UTILS_GRAPHVIEWHELPER_H_
+
+#include <memory>
+#include <set>
+
+#include "aidge/graph/GraphView.hpp"
+#include "aidge/data/Tensor.hpp"
+
+
+namespace Aidge {
+
+/**
+ * @brief Getter for every Producer operator in a GraphView.
+ * @param graphview GraphView instance where Producers should be searched.
+ * @return std::set<std::shared_ptr<Node>>
+ */
+std::set<std::shared_ptr<Tensor>> producers(std::shared_ptr<GraphView> graphview);
+
+
+// TODO: change for every Tensor of Operator Producer not constant
+/**
+ * @brief Getter for every ``Tensor`` owned by an ``Operator`` inside the provided ``GraphView``.
+ * @note An ``Operator`` owns its output ``Tensor``s.
+ *
+ * @param graphview Pointer to the ``GraphView`` from which ``Tensor``s should be extracted.
+ * @return std::set<std::shared_ptr<Tensor>> Set of pointers to the ``Tensor``s.
+ */
+std::set<std::shared_ptr<Tensor>> parameters(std::shared_ptr<GraphView> graphview);
+
+void compile_gradient(std::shared_ptr<Aidge::GraphView> gv);
+
+} // namespace Aidge
+
+#endif /* AIDGE_CORE_UTILS_GRAPHVIEWHELPER_H_ */
diff --git a/include/aidge/recipies/LabelGraph.hpp b/include/aidge/recipes/LabelGraph.hpp
similarity index 90%
rename from include/aidge/recipies/LabelGraph.hpp
rename to include/aidge/recipes/LabelGraph.hpp
index 9dd77e5e9f397260cf936cf77b15616c17ea33b8..61f04c313bb4c12861b2a57299761208124e9cbf 100644
--- a/include/aidge/recipies/LabelGraph.hpp
+++ b/include/aidge/recipes/LabelGraph.hpp
@@ -9,8 +9,8 @@
  *
  ********************************************************************************/
 
-#ifndef AIDGE_RECIPIES_LABELGRAPH_H_
-#define AIDGE_RECIPIES_LABELGRAPH_H_
+#ifndef AIDGE_RECIPES_LABELGRAPH_H_
+#define AIDGE_RECIPES_LABELGRAPH_H_
 
 #include "aidge/graph/GraphView.hpp"
 #include "aidge/graph/Node.hpp"
@@ -32,4 +32,4 @@ NodePtr nodeLabel(NodePtr node);
 std::shared_ptr<GraphView> labelGraph(std::shared_ptr<GraphView> graph);
 } // namespace Aidge
 
-#endif /* AIDGE_RECIPIES_LABELGRAPH_H_ */
+#endif /* AIDGE_RECIPES_LABELGRAPH_H_ */
diff --git a/include/aidge/recipies/Recipies.hpp b/include/aidge/recipes/Recipes.hpp
similarity index 88%
rename from include/aidge/recipies/Recipies.hpp
rename to include/aidge/recipes/Recipes.hpp
index fb4bc22c69ec2b4e8dcc6178c9fcda0a85190f78..97c608cd38ca76a4f40b8fb02282751a97ceed4e 100644
--- a/include/aidge/recipies/Recipies.hpp
+++ b/include/aidge/recipes/Recipes.hpp
@@ -9,8 +9,8 @@
  *
  ********************************************************************************/
 
-#ifndef AIDGE_CORE_UTILS_RECIPIES_H_
-#define AIDGE_CORE_UTILS_RECIPIES_H_
+#ifndef AIDGE_CORE_UTILS_RECIPES_H_
+#define AIDGE_CORE_UTILS_RECIPES_H_
 
 #include <memory>
 #include <set>
@@ -22,6 +22,8 @@
 
 namespace Aidge {
 
+void constantFolding(std::shared_ptr<GraphView> graph);
+
 // FUSE MATMUL + ADD -> FC
 
 /**
@@ -114,7 +116,13 @@ std::set<std::shared_ptr<Node>> getConvHorizontalTiling(const std::shared_ptr<No
 */
 void explicitCastMove(std::shared_ptr<GraphView> graphView);
 
+/**
+ * Flatten the graph by replacing the meta operators by their micro graph.
+ * @param recursive If true, recursively replace meta operators until there is
+ * no more meta operator in the graph.
+*/
+void expandMetaOps(std::shared_ptr<GraphView> graph, bool recursive = false);
 
 } // namespace Aidge
 
-#endif /* AIDGE_CORE_UTILS_RECIPIES_H_ */
+#endif /* AIDGE_CORE_UTILS_RECIPES_H_ */
diff --git a/include/aidge/recipies/GraphViewHelper.hpp b/include/aidge/recipies/GraphViewHelper.hpp
deleted file mode 100644
index d7bcec713087054640c87c6fd229fee53d1ed4a6..0000000000000000000000000000000000000000
--- a/include/aidge/recipies/GraphViewHelper.hpp
+++ /dev/null
@@ -1,40 +0,0 @@
-/********************************************************************************
- * Copyright (c) 2023 CEA-List
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * SPDX-License-Identifier: EPL-2.0
- *
- ********************************************************************************/
-
-#ifndef AIDGE_CORE_UTILS_RECIPIES_H_
-#define AIDGE_CORE_UTILS_RECIPIES_H_
-
-#include <memory>
-#include <set>
-
-#include "aidge/graph/Node.hpp"
-#include "aidge/graph/GraphView.hpp"
-
-
-namespace Aidge {
-
-/**
- * @brief Getter for every Producer operator in a GraphView.
- * @param graphview GraphView instance where Producers should be searched.
- * @return std::set<std::shared_ptr<Node>>
- */
-std::set<std::shared_ptr<Aidge::Node>> producers(std::shared_ptr<Aidge::GraphView> graphview) {
-    std::set<std::shared_ptr<Node>> res;
-    const std::set<std::shared_ptr<Node>> nodes = graphview->getNodes();
-
-    std::copy_if(nodes.cbegin(),
-                    nodes.cend(),
-                    std::inserter(res, res.begin()),
-                    [](std::shared_ptr<Node> n){ return n->type() == "Producer"; });
-
-    return res;
-}
-} // namespace Aidge
\ No newline at end of file
diff --git a/include/aidge/scheduler/MemoryManager.hpp b/include/aidge/scheduler/MemoryManager.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..360b01f76e7a9b51f36b83d4d35286eced35016a
--- /dev/null
+++ b/include/aidge/scheduler/MemoryManager.hpp
@@ -0,0 +1,328 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CORE_SCHEDULER_MEMORY_MANAGER_H
+#define AIDGE_CORE_SCHEDULER_MEMORY_MANAGER_H
+
+#include <memory>
+#include <vector>
+#include <map>
+
+#include "aidge/graph/Node.hpp"
+
+namespace Aidge {
+class MemoryManager {
+public:
+    typedef int Clock_T;
+
+    enum OptimizeStrategy {
+        None,
+        OptimizeMaxLifetimeMinSizeFirst,
+        OptimizeMaxLifetimeMaxSizeFirst,
+        OptimizeMaxHoleMaxLifetimeFirst
+    };
+
+    // MemorySpace are contiguous, non-overlapping memory blocks, that can be
+    // re-arranged freely.
+    struct MemorySpace {
+        MemorySpace(Clock_T clock_,
+                    unsigned int offset_,
+                    unsigned int size_,
+                    std::set<std::shared_ptr<Node> > dependencies_
+                        = std::set<std::shared_ptr<Node> >()
+        ):
+            offset(offset_),
+            size(size_),
+            dependencies(dependencies_),
+            allocated(clock_),
+            released(-1) {}
+
+        unsigned int offset;
+        unsigned int size;
+        std::set<std::shared_ptr<Node> > dependencies;
+        Clock_T allocated;
+        Clock_T released;
+    };
+
+    // MemoryPlane belongs to a MemorySpace. Any number of potentially
+    // overlapping planes can be associated to a MemorySpace.
+    // MemoryPlane can be non-contiguous (in case of stride, or wrapping, when
+    // offset + size > memSpace.size).
+    // MemoryPlane cannot be re-arranged inside a MemorySpace.
+    struct MemoryPlane {
+        MemoryPlane(std::shared_ptr<MemorySpace> memSpace_,
+                    Clock_T clock_,
+                    unsigned int offset_,
+                    unsigned int size_,
+                    unsigned int stride_ = 0,
+                    unsigned int length_ = 1,
+                    unsigned int count_ = 1
+        ):
+            memSpace(memSpace_),
+            allocated(clock_),
+            offset(offset_),
+            size(size_),
+            stride(std::max(size_, stride_)),
+            length(length_),
+            count(count_)
+        {
+            assert(offset <= memSpace->size);
+            // The preceding assert should allow offset == memSpace->size (see
+            // issue #63). This means immediate wrapping.
+            // It appends if the final offset computed in reallocate() is at
+            // the end of the previous memPlane and is also at the end of the
+            // memSpace (in case for example of in-place memory op.).
+            // Instead of bringing the offset back to the beginning of the
+            // memSpace, we stay attached to this offset in case the memSpace
+            // grows when a new memPlane is added.
+
+            assert(getContiguousOffset() >= memSpace->offset);
+            assert(getWrappedOffset() >= memSpace->offset);
+            assert(getContiguousOffset() + getContiguousSize()
+                <= memSpace->offset + memSpace->size);
+            assert(getWrappedOffset() + getWrappedSize()
+                <= memSpace->offset + memSpace->size);
+        }
+
+        inline unsigned int getSize() const {
+            return stride * length * count;
+        }
+
+        inline unsigned int getUsefulSize() const {
+            return size * length * count;
+        }
+
+        inline unsigned int getContiguousOffset() const {
+            return memSpace->offset + offset;
+        }
+
+        inline unsigned int getContiguousSize() const {
+            return std::min(getSize(), getLimit());
+        }
+
+        inline unsigned int getWrappedOffset() const {
+            return memSpace->offset;
+        }
+
+        inline unsigned int getWrappedSize() const {
+            return getSize() - getContiguousSize();
+        }
+
+        inline unsigned int getFinalOffset() const {
+            return (getWrappedSize() > 0)
+                ? getWrappedOffset() + getWrappedSize()
+                : getContiguousOffset() + getContiguousSize();
+        }
+
+        inline unsigned int getUpperOffset() const {
+            return (getContiguousOffset() + getContiguousSize());
+        }
+
+        // Limit is computed dynamically, as memSpace->size may increase after
+        // the creation of this memory space. This is actually necessary to
+        // ensure that the memory wrapping works correctly, because when
+        // computing the margin required for the wrapping, it is assumed that
+        // the previous layer wrapping extends to the full memory space size.
+        inline unsigned int getLimit() const {
+            // limit must be a multiple of (stride * length) if count > 1
+            // or stride if length > 1
+            // uses floor() to stay below memSpace->size
+            return (count > 1)
+                ? std::floor((memSpace->size - offset)
+                        / static_cast<double>(stride * length)) * (stride * length)
+                : (length > 1)
+                    ? std::floor((memSpace->size - offset)
+                            / static_cast<double>(stride)) * stride
+                    : memSpace->size - offset;
+        }
+
+        std::shared_ptr<MemorySpace> memSpace;
+        Clock_T allocated;
+        unsigned int offset;
+        unsigned int size;
+        unsigned int stride;
+        unsigned int length;
+        unsigned int count;
+    };
+
+    struct MaxLifetimeMinSizeFirst {
+        MaxLifetimeMinSizeFirst(unsigned int maxLifetime_)
+            : maxLifetime(maxLifetime_) {}
+        const unsigned int maxLifetime;
+
+        bool operator()(const std::shared_ptr<MemorySpace>& p0,
+                        const std::shared_ptr<MemorySpace>& p1);
+    };
+
+    struct MaxLifetimeMaxSizeFirst {
+        MaxLifetimeMaxSizeFirst(unsigned int maxLifetime_)
+            : maxLifetime(maxLifetime_) {}
+        const unsigned int maxLifetime;
+
+        bool operator()(const std::shared_ptr<MemorySpace>& p0,
+                        const std::shared_ptr<MemorySpace>& p1);
+    };
+
+    struct MaxHoleMaxLifetimeFirst {
+        MaxHoleMaxLifetimeFirst(unsigned int maxLifetime_, MemoryManager* inst_)
+            : maxLifetime(maxLifetime_),
+              inst(inst_) {}
+        const unsigned int maxLifetime;
+        MemoryManager* inst;
+
+        bool operator()(const std::shared_ptr<MemorySpace>& p0,
+                        const std::shared_ptr<MemorySpace>& p1);
+    };
+
+    struct CompByNodeName {
+        bool operator()(const std::shared_ptr<Node>& lhs,
+                        const std::shared_ptr<Node>& rhs) const
+        {
+            return lhs->name() < rhs->name();
+        }
+    };
+
+    typedef std::map<std::shared_ptr<Node>, std::vector<MemoryPlane>,
+        CompByNodeName> MemMap_T;
+
+public:
+    MemoryManager(): mClock(0) {}
+    ~MemoryManager() noexcept;
+
+public:
+    /// Generates a new MemorySpace
+    std::shared_ptr<MemorySpace> reserve(unsigned int size,
+                                    const std::set<std::shared_ptr<Node> >&
+                          dependencies = std::set<std::shared_ptr<Node> >());
+    /// Expand an existing MemorySpace, without affecting its MemoryPlane
+    /// This function rebuild the memory stack mMemStack
+    void expand(std::shared_ptr<MemorySpace> memSpace,
+                unsigned int requiredSize);
+    /// Generates a MemoryPlane in a new MemorySpace
+    MemoryPlane allocate(unsigned int size,
+                         const std::set<std::shared_ptr<Node> >&
+                          dependencies = std::set<std::shared_ptr<Node> >(),
+                         unsigned int stride = 0,
+                         unsigned int length = 1,
+                         unsigned int count = 1);
+    /// Generates a MemoryPlane in a new MemorySpace, associated to a Node
+    unsigned int allocate(const std::shared_ptr<Node>& node,
+                          unsigned int size,
+                          const std::set<std::shared_ptr<Node> >&
+                          dependencies = std::set<std::shared_ptr<Node> >(),
+                          unsigned int stride = 0,
+                          unsigned int length = 1,
+                          unsigned int count = 1);
+    bool isWrapAround(std::shared_ptr<MemorySpace> memSpace,
+                      unsigned int offset,
+                      unsigned int size,
+                      unsigned int stride = 0,
+                      unsigned int length = 1,
+                      unsigned int count = 1) const;
+    /// Generate a new MemoryPlane in an existing MemorySpace
+    MemoryPlane reallocate(std::shared_ptr<MemorySpace> memSpace,
+                           unsigned int offset,
+                           unsigned int size,
+                           bool wrapAround,
+                           unsigned int extraSize = 0,
+                           const std::set<std::shared_ptr<Node> >&
+                additionalDependencies = std::set<std::shared_ptr<Node> >(),
+                           unsigned int stride = 0,
+                           unsigned int length = 1,
+                           unsigned int count = 1);
+    /// Generate a new MemoryPlane directly following an existing MemoryPlane
+    /// memPlane with an additionnal offset extraOffset
+    MemoryPlane reallocate(const MemoryPlane& memPlane,
+                           unsigned int extraOffset,
+                           unsigned int size,
+                           bool wrapAround,
+                           unsigned int extraSize = 0,
+                           const std::set<std::shared_ptr<Node> >&
+                additionalDependencies = std::set<std::shared_ptr<Node> >(),
+                           unsigned int stride = 0,
+                           unsigned int length = 1,
+                           unsigned int count = 1);
+    /// Generate a new MemoryPlane in an existing MemorySpace, associated to a
+    /// Node
+    unsigned int reallocate(std::shared_ptr<MemorySpace> memSpace,
+                            const std::shared_ptr<Node>& node,
+                            unsigned int offset,
+                            unsigned int size,
+                            bool wrapAround,
+                            unsigned int extraSize = 0,
+                            const std::set<std::shared_ptr<Node> >&
+                additionalDependencies = std::set<std::shared_ptr<Node> >(),
+                            unsigned int stride = 0,
+                            unsigned int length = 1,
+                            unsigned int count = 1);
+    /// Generate a new MemoryPlane directly following an existing MemoryPlane
+    /// memPlane with an additionnal offset extraOffset
+    unsigned int reallocate(const MemoryPlane& memPlane,
+                            const std::shared_ptr<Node>& node,
+                            unsigned int extraOffset,
+                            unsigned int size,
+                            bool wrapAround,
+                            unsigned int extraSize = 0,
+                            const std::set<std::shared_ptr<Node> >&
+                additionalDependencies = std::set<std::shared_ptr<Node> >(),
+                            unsigned int stride = 0,
+                            unsigned int length = 1,
+                            unsigned int count = 1);
+
+    unsigned int release(std::shared_ptr<MemorySpace> memSpace);
+    unsigned int release(const std::shared_ptr<Node>& node);
+    unsigned int releaseDependencies(const std::shared_ptr<Node>& node);
+    void optimize(OptimizeStrategy strategy);
+    unsigned int getOffset(const std::shared_ptr<Node>& node,
+                           unsigned int plane = 0) const;
+    unsigned int getSize(const std::shared_ptr<Node>& node,
+                         unsigned int plane) const;
+    unsigned int getSize(const std::shared_ptr<Node>& node) const;
+    unsigned int getNbPlanes(const std::shared_ptr<Node>& node) const;
+    unsigned int getPeakUsage() const;
+    Clock_T getMaxLifetime() const;
+    const std::vector<MemoryPlane>& getPlanes(const std::shared_ptr<Node>& node)
+        const;
+    const MemMap_T& getPlanes() const { return mMemPlanes; }
+    MemMap_T getPlanes(std::shared_ptr<MemorySpace> memSpace) const;
+    unsigned int getNbPlanes(std::shared_ptr<MemorySpace> memSpace) const;
+    Clock_T getCurrentTick() const { return mClock; };
+    void tick();
+    void log(const std::string& fileName) const;
+
+private:
+    /// Find a valid offset in the memory stack that can fit a contiguous chunk
+    /// of memory of size @size
+    unsigned int onStack(unsigned int size);
+    unsigned int offStack(unsigned int offset);
+    std::map<unsigned int, unsigned int> getStack(
+        std::shared_ptr<MemorySpace> memSpace,
+        Clock_T clock) const;
+    std::pair<Clock_T, unsigned int> getMaxHole(
+        std::shared_ptr<MemorySpace> memSpace) const;
+
+    std::map<unsigned int, unsigned int> mMemStack;
+    std::vector<std::shared_ptr<MemorySpace> > mMemSpaces;
+    MemMap_T mMemPlanes;
+    Clock_T mClock;
+};
+}
+
+namespace {
+template <>
+const char* const EnumStrings<Aidge::MemoryManager::OptimizeStrategy>::data[]
+    = {"None",
+       "OptimizeMaxLifetimeMinSizeFirst",
+       "OptimizeMaxLifetimeMaxSizeFirst",
+       "OptimizeMaxHoleMaxLifetimeFirst"};
+}
+
+#endif // AIDGE_CORE_SCHEDULER_MEMORY_MANAGER_H
diff --git a/include/aidge/scheduler/ParallelScheduler.hpp b/include/aidge/scheduler/ParallelScheduler.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..0b6f963d61bf0079a9a32bd335ba765788aba2a5
--- /dev/null
+++ b/include/aidge/scheduler/ParallelScheduler.hpp
@@ -0,0 +1,44 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CORE_SCHEDULER_PARALLELSCHEDULER_H_
+#define AIDGE_CORE_SCHEDULER_PARALLELSCHEDULER_H_
+
+#include <chrono>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+#include <map>
+
+#include "aidge/scheduler/Scheduler.hpp"
+
+namespace Aidge {
+/**
+ * Multi-threaded parallel scheduler with dynamic scheduling.
+*/
+class ParallelScheduler : public Scheduler {
+public:
+    ParallelScheduler(std::shared_ptr<GraphView> graphView, std::shared_ptr<Node> upperNode = nullptr)
+        : Scheduler(graphView, upperNode)
+    {
+        // ctor
+    };
+    ~ParallelScheduler() = default;
+
+    /**
+     * @brief Run the provided Computational Graph with a batch of data
+     */
+    virtual void forward(bool forwardDims = true, std::vector<std::shared_ptr<Aidge::Tensor>> data = {});
+};
+} // namespace Aidge
+
+#endif /* AIDGE_CORE_SCHEDULER_PARALLELSCHEDULER_H_ */
diff --git a/include/aidge/scheduler/Scheduler.hpp b/include/aidge/scheduler/Scheduler.hpp
index 6dcec5aaa4fa80aefebd538a1728445051ca080e..2f8fbb7aeb6562e0dd309f8f53def6d0fed5a08a 100644
--- a/include/aidge/scheduler/Scheduler.hpp
+++ b/include/aidge/scheduler/Scheduler.hpp
@@ -9,85 +9,167 @@
  *
  ********************************************************************************/
 
-#ifndef AIDGE_SCHEDULER_H_
-#define AIDGE_SCHEDULER_H_
+#ifndef AIDGE_CORE_SCHEDULER_SCHEDULER_H_
+#define AIDGE_CORE_SCHEDULER_SCHEDULER_H_
 
+#include <cstddef>  // std::size_t
 #include <chrono>
+#include <map>
 #include <memory>
 #include <set>
 #include <string>
 #include <vector>
 
+#include "aidge/data/Tensor.hpp"
+#include "aidge/scheduler/MemoryManager.hpp"
+#include "aidge/utils/Types.h"
+
 namespace Aidge {
 class Node;
 class GraphView;
 
-class SequentialScheduler {
-private:
+class Scheduler {
+protected:
+    struct StaticSchedulingElement {
+        StaticSchedulingElement(
+            std::shared_ptr<Node> node_,
+            std::size_t early_ = static_cast<std::size_t>(-1),
+            std::size_t late_ = static_cast<std::size_t>(-1))
+            : node(node_), early(early_), late(late_) {}
+
+        std::shared_ptr<Node> node;
+        std::size_t early;
+        std::size_t late;
+        std::vector<std::shared_ptr<StaticSchedulingElement>> earlierThan;
+        std::vector<std::shared_ptr<StaticSchedulingElement>> laterThan;
+    };
+
+    /**
+     * @brief Node with its start/end execution time stored for later display.
+     */
     struct SchedulingElement {
         SchedulingElement(
             std::shared_ptr<Node> node_,
             std::chrono::time_point<std::chrono::high_resolution_clock> start_,
             std::chrono::time_point<std::chrono::high_resolution_clock> end_)
             : node(node_), start(start_), end(end_) {}
-
+        ~SchedulingElement() noexcept = default;
         std::shared_ptr<Node> node;
         std::chrono::time_point<std::chrono::high_resolution_clock> start;
         std::chrono::time_point<std::chrono::high_resolution_clock> end;
     };
+public:
+    struct PriorProducersConsumers {
+        PriorProducersConsumers();
+        PriorProducersConsumers(const PriorProducersConsumers&);
+        ~PriorProducersConsumers() noexcept;
+        bool isPrior = false;
+        std::set<std::shared_ptr<Aidge::Node>> requiredProducers;
+        std::set<std::shared_ptr<Aidge::Node>> priorConsumers;
+    };
 
 public:
-    SequentialScheduler(std::shared_ptr<GraphView> graphView)
-        : mGraphView(graphView)
+    Scheduler(std::shared_ptr<GraphView> graphView, std::shared_ptr<Node> upperNode = nullptr)
+        : mGraphView(graphView),
+          mUpperNode(upperNode)
     {
         // ctor
     };
-    ~SequentialScheduler() = default;
 
-    void generateScheduling(bool verbose = false);
-    inline void resetScheduling() {
-        mScheduling.clear();
-        mStaticSchedule.clear();
+    virtual ~Scheduler() noexcept;
+
+public:
+    /**
+     * @brief Return a vector of Node ordered by the order they are called by the scheduler.
+     * @return std::vector<std::shared_ptr<Node>>
+     */
+    std::vector<std::shared_ptr<Node>> getStaticScheduling(std::size_t step = 0) const;
+
+    inline std::shared_ptr<GraphView> graphView() const noexcept {
+        return mGraphView;
     }
 
     /**
-     * @brief Run the provided Computational Graph with a batch of data
+     * @brief Generate full static scheduling of the GraphView.
+     * For each node, an earliest and latest possible execution logical step
+     * is specified. Nodes that may be scheduled at the same logical step have
+     * no data dependency and can be run in parallel.
+    */
+    void generateScheduling();
+
+    /**
+     * Reset all scheduling and associated nodes producer consumer.
+    */
+    void resetScheduling();
+
+    /**
+     * Generate the memory layout for the current static scheduling.
+     * @param incProducers If true, include the producers in the memory layout.
+     * @param wrapAroundBuffer If true, allow wrapping in memory planes.
+    */
+    MemoryManager generateMemory(bool incProducers = false, bool wrapAroundBuffer = false) const;
+
+    /**
+     * @brief Place the data tensors inside in the data input tensor of the graphView. In case of multiple data input tensors, they are mapped to producers in the order given by the graph.
+     *
+     * @param data data input tensors
      */
-    void forward(bool forwardDims = true, bool verbose = false);
+    void connectInputs(std::vector<std::shared_ptr<Aidge::Tensor>> data);
 
     /**
-     * @brief Save in a Markdown file the order of layers execution.
+     * @brief Save in a Markdown file the static scheduling with early and late relative order for the nodes.
      * @param fileName Name of the generated file.
      */
-    void saveSchedulingDiagram(const std::string& fileName) const;
+    void saveStaticSchedulingDiagram(const std::string& fileName) const;
 
     /**
-     * @brief Return a vector of Node ordered by the order they are called by the scheduler
-     * @return std::vector<std::shared_ptr<Node>>
+     * @brief Save in a Markdown file the order of layers execution.
+     * @param fileName Name of the generated file.
      */
-    inline std::vector<std::shared_ptr<Node>> getStaticScheduling() const noexcept {
-        return mStaticSchedule;
-    }
-    inline std::shared_ptr<GraphView> getGraphView() const noexcept {
-        return mGraphView;
-    }
+    void saveSchedulingDiagram(const std::string& fileName) const;
 
-private:
+
+protected:
     /**
-     * @brief Set of layers receiving an input from currently processing layers
-     *
-     * @param producers Set of layers ready to run.
-     * @return std::set<std::shared_ptr<Node>>
+     * @brief Getter for the set of children Nodes of the given input Nodes.
+     * @param producers Set of Nodes for which we want to obtain the set of children Nodes.
+     * @return std::set<std::shared_ptr<Node>> Children Nodes.
      */
     std::set<std::shared_ptr<Node>> getConsumers(const std::set<std::shared_ptr<Node>>& producers) const;
 
+    Elts_t getNbAvailableData(const std::shared_ptr<Node>& node, const IOIndex_t inputIdx) const;
+
+    PriorProducersConsumers getPriorProducersConsumers(const std::shared_ptr<Node>& node) const;
+
+    /**
+     * @brief Generate an initial base scheduling for the GraphView.
+     * The scheduling is entirely sequential and garanteed to be valid w.r.t.
+     * each node producer-consumer model.
+    */
+    std::vector<std::shared_ptr<StaticSchedulingElement>> generateBaseScheduling() const;
+
+    /**
+     * Fill-in early and late scheduling step from initial base scheduling.
+     * For each node, specifies the earliest and latest possible execution
+     * logical step.
+    */
+    void generateEarlyLateScheduling(std::vector<std::shared_ptr<StaticSchedulingElement>>& schedule) const;
+
+private:
+    void summarizeConsumerState(const std::shared_ptr<Node>& consumer, const std::string& nodeName) const;
+
+protected:
     /** @brief Shared ptr to the scheduled graph view */
     std::shared_ptr<GraphView> mGraphView;
+    /** @brief Shared ptr to the upper node containing the graph view */
+    std::weak_ptr<Node> mUpperNode;
     /** @brief List of SchedulingElement (i.e: Nodes with their computation time) */
     std::vector<SchedulingElement> mScheduling;
     /** @brief List of nodes ordered by their */
-    std::vector<std::shared_ptr<Node>> mStaticSchedule;
+    std::vector<std::vector<std::shared_ptr<StaticSchedulingElement>>> mStaticSchedule;
+    std::size_t mStaticScheduleStep = 0;
+    mutable std::map<std::shared_ptr<Node>, PriorProducersConsumers> mPriorCache;
 };
 } // namespace Aidge
 
-#endif /* AIDGE_SCHEDULER_H_ */
+#endif /* AIDGE_CORE_SCHEDULER_SCHEDULER_H_ */
diff --git a/include/aidge/scheduler/SequentialScheduler.hpp b/include/aidge/scheduler/SequentialScheduler.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..9cf0c2c1877bbbe5930c6b1e39f2a46c33e21d93
--- /dev/null
+++ b/include/aidge/scheduler/SequentialScheduler.hpp
@@ -0,0 +1,64 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CORE_SCHEDULER_SEQUENTIALSCHEDULER_H_
+#define AIDGE_CORE_SCHEDULER_SEQUENTIALSCHEDULER_H_
+
+#include <memory>
+#include <vector>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/graph/GraphView.hpp"
+#include "aidge/graph/Node.hpp"
+#include "aidge/scheduler/Scheduler.hpp"
+
+namespace Aidge {
+/**
+ * Multi-threaded parallel scheduler with dynamic scheduling.
+*/
+class SequentialScheduler : public Scheduler {
+public:
+    enum class SchedulingPolicy {
+        Default,
+        AsSoonAsPossible,
+        AsLateAsPossible
+    };
+
+public:
+    SequentialScheduler(std::shared_ptr<GraphView> graphView, std::shared_ptr<Node> upperNode = nullptr)
+        : Scheduler(graphView, upperNode),
+          mSchedulingPolicy(SchedulingPolicy::Default)
+    {
+        // ctor
+    };
+
+    ~SequentialScheduler() = default;
+
+public:
+    inline void setSchedulingPolicy(SchedulingPolicy policy) {
+        mSchedulingPolicy = policy;
+    }
+    /**
+     * @brief Run the provided Computational Graph with a batch of data
+     */
+    virtual void forward(bool forwardDims = true, std::vector<std::shared_ptr<Aidge::Tensor>> data = {});
+
+    /**
+     * @brief Run the provided Computational Graph with a batch of data
+     */
+    void backward(std::vector<std::shared_ptr<Aidge::Tensor>> data, bool instantiateGrad = true);
+
+private:
+    SchedulingPolicy mSchedulingPolicy;
+};
+} // namespace Aidge
+
+#endif /* AIDGE_CORE_SCHEDULER_SEQUENTIALSCHEDULER_H_ */
diff --git a/include/aidge/scheduler/ThreadPool.hpp b/include/aidge/scheduler/ThreadPool.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..e016ad4f3eead6e5bfd6d2b994e6e9cb43e61af9
--- /dev/null
+++ b/include/aidge/scheduler/ThreadPool.hpp
@@ -0,0 +1,42 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CORE_SCHEDULER_THREADPOOL_H_
+#define AIDGE_CORE_SCHEDULER_THREADPOOL_H_
+
+#include <thread>
+#include <mutex>
+#include <queue>
+#include <vector>
+#include <functional>
+#include <condition_variable>
+#include <atomic>
+
+namespace Aidge {
+class ThreadPool {
+public:
+    ThreadPool(size_t nbThreads = std::thread::hardware_concurrency());
+    void queueJob(const std::function<void()>& job);
+    bool busy();
+    virtual ~ThreadPool();
+
+private:
+    void threadLoop();
+
+    bool mTerminate = false;
+    std::mutex mQueueMutex;
+    std::condition_variable mMutexCondition;
+    std::vector<std::thread> mThreads;
+    std::queue<std::function<void()>> mJobs;
+};
+} // namespace Aidge
+
+#endif /* AIDGE_CORE_SCHEDULER_THREADPOOL_H_ */
diff --git a/include/aidge/stimuli/Stimulus.hpp b/include/aidge/stimuli/Stimulus.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..80e7c76d4857f577f30b90588f4c3998be80bdb8
--- /dev/null
+++ b/include/aidge/stimuli/Stimulus.hpp
@@ -0,0 +1,107 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CORE_STIMULI_STIMULUS_H_
+#define AIDGE_CORE_STIMULI_STIMULUS_H_
+
+#include <string>
+#include <memory>
+#include <tuple>
+
+#include "aidge/backend/StimulusImpl.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+
+namespace Aidge {
+/**
+ * @brief Stimulus. A class wrapping a data sample. Stimulus has two functioning modes. The first mode enables to load data samples from a dataPath and optionnaly store the data in-memory. The second mode enables to store a data sample that was already loaded in memory.
+ * @details When Stimulus is used in the first mode, the loading function is determined automaticaly based on the backend and the file extension.
+ */
+class Stimulus : public Registrable<Stimulus, std::tuple<std::string, std::string>, std::unique_ptr<StimulusImpl>(const std::string&)> {
+private:
+    /// Stimulus data path
+    const std::string mDataPath;
+    const std::string mFileExtension;
+    bool mLoadDataInMemory;
+
+    /// Stimulus data ptr
+    std::shared_ptr<Tensor> mData;
+
+    // Implementation of the Stimulus
+    std::unique_ptr<StimulusImpl> mImpl;
+
+public:
+    Stimulus() = delete;
+
+    /**
+     * @brief Construct a new Stimulus object based on a tensor that is already loaded in memory.
+     *
+     * @param data the data tensor.
+     */
+    Stimulus(const std::shared_ptr<Tensor> data)
+    : mLoadDataInMemory(true),
+      mData(data)
+    {
+        // ctor
+    }
+
+    /**
+     * @brief Construct a new Stimulus object based on a dataPath to load the data.
+     *
+     * @param dataPath path to the data to be loaded.
+     * @param loadDataInMemory when true, keep the data in memory once loaded
+     */
+    Stimulus(const std::string& dataPath, bool loadDataInMemory = false)
+    : mDataPath(dataPath),
+      mFileExtension(dataPath.substr(dataPath.find_last_of(".") + 1)),
+      mLoadDataInMemory(loadDataInMemory)
+    {
+        AIDGE_ASSERT((dataPath.find_last_of(".") !=  std::string::npos), "Cannot find extension");
+    }
+
+    /**
+     * @brief Construct a new Stimulus object copied from another one.
+     * @param otherStimulus
+     */
+    Stimulus(const Stimulus& otherStimulus)
+        : mDataPath(otherStimulus.mDataPath),
+          mFileExtension(otherStimulus.mFileExtension),
+          mLoadDataInMemory(otherStimulus.mLoadDataInMemory),
+          mData(otherStimulus.mData)
+    {
+        if (otherStimulus.mImpl) {
+            mImpl = Registrar<Stimulus>::create({"opencv", mFileExtension})(mDataPath);
+        }
+    }
+
+    virtual ~Stimulus();
+
+public:
+    /**
+     * @brief Set the backend of the stimuli associated load implementation
+     * @details Create and initialize an implementation.
+     * @param name name of the backend.
+     */
+    inline void setBackend(const std::string &name) {
+        mImpl = Registrar<Stimulus>::create({name, mFileExtension})(mDataPath);
+    }
+
+    /**
+     * @brief Get the data tensor associated to the stimuli. The data is either loaded from a datapath or passed from an in-memory tensor.
+     *
+     * @return std::shared_ptr<Tensor> the data tensor.
+     */
+    virtual std::shared_ptr<Tensor> load();
+};
+} // namespace Aidge
+
+#endif // AIDGE_CORE_STIMULI_STIMULUS_H_
diff --git a/include/aidge/utils/Attributes.hpp b/include/aidge/utils/Attributes.hpp
index d3444000191022b575adaf1430319479daa5d4fc..927686cfd5cca910c5ffb25364ae4bc971ad18bf 100644
--- a/include/aidge/utils/Attributes.hpp
+++ b/include/aidge/utils/Attributes.hpp
@@ -69,6 +69,11 @@ public:
     *  be agnostic from its return type.
     */
     virtual py::object getAttrPy(const std::string& name) const = 0;
+    /* Bindable set function, does not recquire any templating.
+    *  This is thanks to py::object which allow the function to
+    *  be agnostic from ``value`` type.
+    */
+    virtual void setAttrPy(const std::string& name, py::object&& value) = 0;
 #endif
     virtual ~Attributes() {}
 };
diff --git a/include/aidge/utils/Directories.hpp b/include/aidge/utils/Directories.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..3bc07b9dd58e472096102c1b0c66971164d632a3
--- /dev/null
+++ b/include/aidge/utils/Directories.hpp
@@ -0,0 +1,83 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+
+#ifndef AIDGE_DIRECTORIES_H_
+#define AIDGE_DIRECTORIES_H_
+
+
+#include <string>  // std::string
+#include <sstream> // std::stringstream
+#include <iostream>
+#include <sys/stat.h>
+#include <errno.h>
+
+#ifdef WIN32
+#include <direct.h>
+#else
+#include <sys/types.h>
+#include <unistd.h>
+#endif
+
+namespace Aidge {
+
+    bool isNotValidFilePath(int c) {
+        return (iscntrl(c)
+            || c == '<'
+            || c == '>'
+            || c == ':'
+            || c == '"'
+            || c == '|'
+            || c == '?'
+            || c == '*');
+    }
+
+    std::string filePath(const std::string& str) {
+        std::string filePath(str);
+        std::replace_if(filePath.begin(), filePath.end(),
+                        isNotValidFilePath, '_');
+        return filePath;
+    }
+
+
+    bool createDirectories(const std::string& dirName)
+    {
+        std::stringstream path(dirName);
+        std::string dir;
+        std::string pathToDir("");
+        int status = 0;
+
+        while (std::getline(path, dir, '/') && status == 0) {
+            pathToDir += dir + '/';
+            struct stat fileStat;
+            if (stat(pathToDir.c_str(), &fileStat) != 0) {
+                // Directory does not exist
+    #ifdef WIN32
+                status = _mkdir(pathToDir.c_str());
+    #else
+    #if defined(S_IRWXU)
+                status = mkdir(pathToDir.c_str(), S_IRWXU | S_IRWXG | S_IRWXO);
+    #else
+                status = mkdir(pathToDir.c_str());
+    #endif
+    #endif
+            } else if (!S_ISDIR(fileStat.st_mode)) {
+                status = -1;
+            }
+        }
+        return (status == 0 || errno == EEXIST);
+    }
+
+
+}
+
+#endif //AIDGE_DIRECTORIES_H_
+
diff --git a/include/aidge/utils/DynamicAttributes.hpp b/include/aidge/utils/DynamicAttributes.hpp
index 2af8f47e9420f266cc6eca21f167944c761db7ea..44c3b1f5e8df833344fa9b7fe72bdb4ef1e0ec12 100644
--- a/include/aidge/utils/DynamicAttributes.hpp
+++ b/include/aidge/utils/DynamicAttributes.hpp
@@ -135,7 +135,7 @@ public:
         assert(res.second && "attribute already exists");
     }
 
-    void setAttrPy(const std::string& name, py::object&& value)
+    void setAttrPy(const std::string& name, py::object&& value) override final
     {
         auto resPy = mAttrsPy.emplace(std::make_pair(name, value));
         if (!resPy.second)
@@ -204,7 +204,7 @@ private:
     // Stores C++ attributes (copy) and Python-only attributes
     // Code should be compiled with -fvisibility=hidden
     // See https://pybind11.readthedocs.io/en/stable/faq.html:
-    // “‘SomeClass’ declared with greater visibility than the type of its 
+    // “‘SomeClass’ declared with greater visibility than the type of its
     // field ‘SomeClass::member’ [-Wattributes]”
     // This map will only be populated if Python interpreter is running
     std::map<std::string, py::object> mAttrsPy;
diff --git a/include/aidge/utils/ErrorHandling.hpp b/include/aidge/utils/ErrorHandling.hpp
index 8fbeff30abecfec0077786b21825b6a6f36677c6..f6a9aefe24a420e261a100041578dd751c4a1ee2 100644
--- a/include/aidge/utils/ErrorHandling.hpp
+++ b/include/aidge/utils/ErrorHandling.hpp
@@ -13,30 +13,21 @@
 #ifndef AIDGE_ERRORHANDLING_H_
 #define AIDGE_ERRORHANDLING_H_
 
-#include <cstdio>
 #include <memory>
+#include <cassert>
 
-#define AIDGE_STRINGIZE_DETAIL(x) #x
-#define AIDGE_STRINGIZE(x) AIDGE_STRINGIZE_DETAIL(x)
+#include <fmt/format.h>
+#include <fmt/ranges.h>
+
+#include "aidge/utils/Log.hpp"
 
 #ifdef NO_EXCEPTION
 #define AIDGE_THROW_OR_ABORT(ex, ...) \
-do { std::printf(__VA_ARGS__); std::abort(); } while (false)
+do { Aidge::Log::fatal(__VA_ARGS__); std::abort(); } while (false)
 #else
 #include <stdexcept>
-#include <memory>
 #define AIDGE_THROW_OR_ABORT(ex, ...) \
-do { \
-    int n = 128; \
-    std::unique_ptr<char[]> formatted; \
-    formatted.reset(new char[n]); \
-    const int len = std::snprintf(formatted.get(), n, __VA_ARGS__); \
-    if (len >= n) { \
-        formatted.reset(new char[len + 1]); \
-        std::snprintf(formatted.get(), len + 1, __VA_ARGS__); \
-    }; \
-    throw ex(formatted.get()); \
-} while (false)
+do { Aidge::Log::fatal(__VA_ARGS__); throw ex(fmt::format(__VA_ARGS__)); } while (false)
 #endif
 
 /**
@@ -45,7 +36,7 @@ do { \
  * If it asserts, it means an user error.
 */
 #define AIDGE_ASSERT(stm, ...) \
-if (!(stm)) { printf("Assertion failed: " AIDGE_STRINGIZE(stm) " in " __FILE__ ":%d", __LINE__); \
+if (!(stm)) { Aidge::Log::error("Assertion failed: " #stm " in {}:{}", __FILE__, __LINE__); \
     AIDGE_THROW_OR_ABORT(std::runtime_error, __VA_ARGS__); }
 
 /**
@@ -54,6 +45,6 @@ if (!(stm)) { printf("Assertion failed: " AIDGE_STRINGIZE(stm) " in " __FILE__ "
  * If it asserts, it means a bug.
 */
 #define AIDGE_INTERNAL_ASSERT(stm) \
-assert((stm) && "Internal assertion failed: " #stm " in " __FILE__ ":" AIDGE_STRINGIZE(__LINE__))
+assert((stm) && "Internal assertion failed")
 
 #endif //AIDGE_ERRORHANDLING_H_
diff --git a/include/aidge/utils/Log.hpp b/include/aidge/utils/Log.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a01f81629c8425f9d860bf1ea03bfe421dbd04fa
--- /dev/null
+++ b/include/aidge/utils/Log.hpp
@@ -0,0 +1,187 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+
+#ifndef AIDGE_LOG_H_
+#define AIDGE_LOG_H_
+
+#include <memory>
+
+#include <fmt/format.h>
+#include <fmt/ranges.h>
+
+#include "aidge/utils/Attributes.hpp"
+
+namespace Aidge {
+/**
+ * Helper to define a context anywhere, hidding the scoped variable name
+ * which has no relevance.
+*/
+#define AIDGE_LOG_CONTEXT(...) const Log::Context logContext_##__LINE__(__VA_ARGS__)
+
+
+template<class U>
+static void discard_args(U parg) {
+    (void)parg;
+}
+template<class U, class... Us>
+static void discard_args(U parg, Us... pargs) {
+    (void)parg;
+    discard_args(pargs...);
+}
+
+/**
+ * Aidge logging class, for displaying and file logging of events.
+*/
+class Log {
+public:
+    enum Level {
+        Debug = 0,
+        Info,
+        Notice,
+        Warn,
+        Error,
+        Fatal
+    };
+
+    class Context {
+    public:
+        template <typename... Args>
+        Context(Args&&... args) {
+            Log::mContext.push_back(fmt::format(std::forward<Args>(args)...));
+        }
+
+        ~Context() {
+            Log::mContext.pop_back();
+        }
+    };
+
+    /**
+     * Detailed messages for debugging purposes, providing information helpful
+     * for developers to trace and identify issues.
+     * Detailed insights of what is appening in an operation, not useful for the
+     * end-user. The operation is performed nominally.
+     * @note This level is disabled at compile time for Release, therefore
+     * inducing no runtime overhead for Release.
+    */
+    template <typename... Args>
+    constexpr static void debug(Args&&... args) {
+#ifndef NDEBUG
+        // only when compiled in Debug
+        log(Debug, fmt::format(std::forward<Args>(args)...));
+#else
+        discard_args(&args...);
+#endif
+    }
+
+    /**
+     * Messages that provide a record of the normal operation, about
+     * the application's state, progress, or important events.
+     * Reports normal start, end and key steps in an operation. The operation is
+     * performed nominally.
+    */
+    template <typename... Args>
+    constexpr static void info(Args&&... args) {
+        log(Info, fmt::format(std::forward<Args>(args)...));
+    }
+
+    /**
+     * Applies to normal but significant conditions that may require monitoring,
+     * like unusual or normal fallback events.
+     * Reports specific paths in an operation. The operation can still be
+     * performed normally.
+    */
+    template <typename... Args>
+    constexpr static void notice(Args&&... args) {
+        log(Notice, fmt::format(std::forward<Args>(args)...));
+    }
+
+    /**
+     * Indicates potential issues or situations that may lead to errors but do
+     * not necessarily cause immediate problems.
+     * Some specific steps of the operation could not be performed, but it can
+     * still provide an exploitable result.
+    */
+    template <typename... Args>
+    constexpr static void warn(Args&&... args) {
+        log(Warn, fmt::format(std::forward<Args>(args)...));
+    }
+
+    /**
+     * Signifies a problem or unexpected condition that the application can
+     * recover from, but attention is needed to prevent further issues.
+     * The operation could not be performed, but it does not prevent potential
+     * further operations.
+    */
+    template <typename... Args>
+    constexpr static void error(Args&&... args) {
+        log(Error, fmt::format(std::forward<Args>(args)...));
+    }
+
+    /**
+     * Represents a critical error or condition that leads to the termination of
+     * the application, indicating a severe and unrecoverable problem.
+     * The operation could not be performed and any further operation is
+     * impossible.
+    */
+    template <typename... Args>
+    constexpr static void fatal(Args&&... args) {
+        log(Fatal, fmt::format(std::forward<Args>(args)...));
+    }
+
+    /**
+     * Set the minimum log level displayed in the console.
+    */
+    constexpr static void setConsoleLevel(Level level) {
+        mConsoleLevel = level;
+    }
+
+    /**
+     * Set the minimum log level saved in the log file.
+    */
+    constexpr static void setFileLevel(Level level) {
+        mFileLevel = level;
+    }
+
+    /**
+     * Set the log file name.
+     * Close the current log file and open the one with the new file name.
+     * If empty, stop logging into a file.
+    */
+    static void setFileName(const std::string& fileName) {
+        if (fileName != mFileName) {
+            mFileName = fileName;
+            mFile.release();
+
+            if (!fileName.empty()) {
+                initFile(fileName);
+            }
+        }
+    }
+
+private:
+    static void log(Level level, const std::string& msg);
+    static void initFile(const std::string& fileName);
+
+    static Level mConsoleLevel;
+    static Level mFileLevel;
+    static std::string mFileName;
+    static std::unique_ptr<FILE, decltype(&std::fclose)> mFile;
+    static std::vector<std::string> mContext;
+};
+}
+
+namespace {
+template <>
+const char *const EnumStrings<Aidge::Log::Level>::data[] = {"Debug", "Info", "Notice", "Warn", "Error", "Fatal"};
+}
+
+#endif //AIDGE_LOG_H_
diff --git a/include/aidge/utils/Random.hpp b/include/aidge/utils/Random.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..73cbd1453b3d840d6da2c58eadd5c5f47e9e9070
--- /dev/null
+++ b/include/aidge/utils/Random.hpp
@@ -0,0 +1,61 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_RANDOM_H_
+#define AIDGE_RANDOM_H_
+
+#include <algorithm>
+#include <random>
+#include <vector>
+namespace Aidge {
+
+namespace Random {
+
+/**
+ * @brief Generator is a class created to handle only one Mersenne Twister
+ * pseudo-random number generator for the whole Aidge framework.
+ *
+ * All of its method are static. You can set a random seed and access the
+ * generator.
+ * By default, the random seed is set to 0 but selected randomly.
+ *
+ */
+class Generator {
+   public:
+    /**
+     * @brief Set a seed to the pseudo-random number generator.
+     *
+     * @return std::mt19937&
+     */
+    static void setSeed(unsigned int seed);
+    static unsigned int getSeed() { return seed; };
+    /**
+     * @brief Return a Mersenne Twister pseudo-random number generator.
+     * You can set the seed of this generator using ``setSeed`` method.
+     *
+     * @return std::mt19937&
+     */
+    static std::mt19937& get() { return generator; };
+
+   private:
+    // Mersenne Twister pseudo-random number generator
+    static std::mt19937 generator;
+    static unsigned int seed;
+};
+
+inline void randShuffle(std::vector<unsigned int>& vec) {
+    std::shuffle(vec.begin(), vec.end(), Aidge::Random::Generator::get());
+}
+
+}  // namespace Random
+}  // namespace Aidge
+
+#endif  // AIDGE_RANDOM_H_
diff --git a/include/aidge/utils/Registrar.hpp b/include/aidge/utils/Registrar.hpp
index 66a07eb0ce21354b20f1ca416cc68d26d9bd6280..a6d1d7a9eb5d88dedaf73564847b0f4fbd797c43 100644
--- a/include/aidge/utils/Registrar.hpp
+++ b/include/aidge/utils/Registrar.hpp
@@ -14,17 +14,25 @@
 
 #ifdef PYBIND
 #include <pybind11/pybind11.h>
+#include <pybind11/stl.h> // declare_registrable key can recquire stl
+#include <pybind11/functional.h>// declare_registrable allow binding of lambda fn
+
 #endif
 
+#include "aidge/utils/ErrorHandling.hpp"
+
 #include <functional>
 #include <map>
-#include <cassert>
+#include <vector>
 
 namespace Aidge {
 #ifdef PYBIND
 namespace py = pybind11;
 #endif
 
+// Abstract class used to test if a class is Registrable.
+class AbstractRegistrable {};
+
 template <class DerivedClass, class Key, class Func> // curiously rucurring template pattern
 class Registrable {
 public:
@@ -55,30 +63,84 @@ struct Registrar {
     typedef typename C::registrar_type registrar_type;
 
     Registrar(const registrar_key& key, registrar_type func) {
-        //printf("REGISTRAR: %s\n", key.c_str());
-        bool newInsert;
-        std::tie(std::ignore, newInsert) = C::registry().insert(std::make_pair(key, func));
+        //fmt::print("REGISTRAR: {}\n", key);
+        // bool newInsert;
+        // std::tie(std::ignore, newInsert) = C::registry().insert(std::make_pair(key, func));
+        C::registry().erase(key);
+        C::registry().insert(std::make_pair(key, func));
         //assert(newInsert && "registrar already exists");
     }
 
     static bool exists(const registrar_key& key) {
-        const auto it = C::registry().find(key);
-        return (it != C::registry().end());
+        return (C::registry().find(key) != C::registry().cend());
     }
 
     static auto create(const registrar_key& key){
         const auto it = C::registry().find(key);
-        assert(it != C::registry().end() && "invalid registrar key");
+        AIDGE_ASSERT(it != C::registry().cend(), "missing or invalid registrar key: {}\nDid you include/import the corresponding module?", key);
 
         return (*it).second;
     }
     static std::vector<registrar_key> getKeys(){
         std::vector<registrar_key> keys;
-        for(auto keyValue : C::registry())
+        for(const auto& keyValue : C::registry())
             keys.push_back(keyValue.first);
         return keys;
     }
 };
+
+#ifdef PYBIND
+/**
+ * @brief Function to define register function for a registrable class
+ * Defined here to have access to this function in every module who wants
+ * to create a new registrable class.
+ *
+ * @tparam C registrable class
+ * @param m pybind module
+ * @param class_name python name of the class
+ */
+template <class C>
+void declare_registrable(py::module& m, const std::string& class_name){
+    typedef typename C::registrar_key registrar_key;
+    typedef typename C::registrar_type registrar_type;
+    m.def(("register_"+ class_name).c_str(), [](registrar_key& key, registrar_type function){
+        Registrar<C>(key, function);
+    })
+    .def(("get_keys_"+ class_name).c_str(), [](){
+        return Registrar<C>::getKeys();
+    });
+}
+#endif
+
+/*
+* This macro allow to set an implementation to an operator
+* This macro is mandatory for using implementation registered in python
+* PyBind when calling create method will do a call to the copy ctor if
+* op is not visible to the python world (if the create method return a python function)
+* See this issue for more information https://github.com/pybind/pybind11/issues/4417
+* Note: using a method to do this is not possible has any call to a function will call
+* the cpy ctor. This is why I used a macro
+* Note: I duplicated
+*             (op).setImpl(Registrar<T_Op>::create(backend_name)(op)); \
+* This is because the py::cast need to be done in the same scope.
+* I know this only empyrically not sure what happens under the hood...
+*
+* If someone wants to find an alternative to this Macro, you can contact me:
+*   cyril.moineau@cea.fr
+*/
+#ifdef PYBIND
+#define SET_IMPL_MACRO(T_Op, op, backend_name) \
+    if(Py_IsInitialized()) { \
+        auto obj = py::cast(&(op)); \
+        (op).setImpl(Registrar<T_Op>::create(backend_name)(op)); \
+    } else { \
+        (op).setImpl(Registrar<T_Op>::create(backend_name)(op)); \
+    }
+#else
+#define SET_IMPL_MACRO(T_Op, op, backend_name)                   \
+    (op).setImpl(Registrar<T_Op>::create(backend_name)(op));
+#endif
+
 }
 
 #endif //AIDGE_CORE_UTILS_REGISTRAR_H_
diff --git a/include/aidge/utils/StaticAttributes.hpp b/include/aidge/utils/StaticAttributes.hpp
index a90a08b01915c461bc8951c08ee2dbd979b957de..6bf59155373cf73d158fce4eb5bda58f7d279e69 100644
--- a/include/aidge/utils/StaticAttributes.hpp
+++ b/include/aidge/utils/StaticAttributes.hpp
@@ -88,25 +88,25 @@ public:
 
     // Runtime access with name
     template <typename R>
-    R& getAttr(const char* name) {
+    R& getAttr(const std::string& name) {
         for (std::size_t i = 0; i < size(EnumStrings<ATTRS_ENUM>::data); ++i) {
-            if (strcmp(EnumStrings<ATTRS_ENUM>::data[i], name) == 0) {
+            if (name == EnumStrings<ATTRS_ENUM>::data[i]) {
                 return getAttr<R>(i);
             }
         }
 
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "attribute \"%s\" not found", name);
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "attribute \"{}\" not found", name);
     }
 
     template <typename R>
-    const R& getAttr(const char* name) const {
+    const R& getAttr(const std::string& name) const {
         for (std::size_t i = 0; i < size(EnumStrings<ATTRS_ENUM>::data); ++i) {
-            if (strcmp(EnumStrings<ATTRS_ENUM>::data[i], name) == 0) {
+            if (name == EnumStrings<ATTRS_ENUM>::data[i]) {
                 return getAttr<R>(i);
             }
         }
 
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "attribute \"%s\" not found", name);
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "attribute \"{}\" not found", name);
     }
 
     template <typename R, std::size_t SIZE = std::tuple_size<std::tuple<T...>>::value>
@@ -116,7 +116,7 @@ public:
                 return reinterpret_cast<R&>(std::get<SIZE-1>(mAttrs));
             }
             else {
-                AIDGE_THROW_OR_ABORT(std::runtime_error, "wrong type for attribute with index %lu", i);
+                AIDGE_THROW_OR_ABORT(std::runtime_error, "wrong type for attribute with index {}", i);
             }
         }
         else {
@@ -136,7 +136,7 @@ public:
                 return reinterpret_cast<const R&>(std::get<SIZE-1>(mAttrs));
             }
             else {
-                AIDGE_THROW_OR_ABORT(std::runtime_error, "wrong type for attribute with index %lu", i);
+                AIDGE_THROW_OR_ABORT(std::runtime_error, "wrong type for attribute with index {}", i);
             }
         }
         else {
@@ -190,7 +190,7 @@ public:
             }
         }
 
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "attribute \"%s\" not found", name.c_str());
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "attribute \"{}\" not found", name);
     }
 
     std::set<std::string> getAttrsName() const override final {
@@ -202,6 +202,22 @@ public:
     }
 
     #ifdef PYBIND
+    /**
+     * @brief Return a set of attributes defined.
+     * This method is used to automatically retrieve attributes in the documentation.
+     * This method is a duplicate of ``getAttrsName`` but static.
+     *
+     * @return std::set<std::string>
+     */
+    static std::set<std::string> staticGetAttrsName() {
+        std::set<std::string> attrsName;
+        for (std::size_t i = 0; i < size(EnumStrings<ATTRS_ENUM>::data); ++i) {
+            attrsName.insert(EnumStrings<ATTRS_ENUM>::data[i]);
+        }
+        return attrsName;
+    }
+
+
     py::object getAttrPy(const std::string& name) const override {
         for (std::size_t i = 0; i < size(EnumStrings<ATTRS_ENUM>::data); ++i) {
             if (name == EnumStrings<ATTRS_ENUM>::data[i]) {
@@ -211,8 +227,23 @@ public:
             }
         }
 
-        AIDGE_THROW_OR_ABORT(py::value_error, "attribute \"%s\" not found", name.c_str());
-    };
+        AIDGE_THROW_OR_ABORT(py::value_error, "attribute \"{}\" not found", name);
+    }
+
+
+    void setAttrPy(const std::string& name, py::object&& value) override final{
+        for (std::size_t i = 0; i < size(EnumStrings<ATTRS_ENUM>::data); ++i) {
+            if (name == EnumStrings<ATTRS_ENUM>::data[i]) {
+                // Cannot update attribute using reference has it would require templating
+                // Use a dirty
+                auto tmpAttr = py::cast(mAttrs);
+                py::detail::accessor_policies::tuple_item::set(tmpAttr, static_cast<py::size_t>(i), value);
+                mAttrs = py::cast<std::tuple<T...>>(tmpAttr);
+                return;
+            }
+        }
+        AIDGE_THROW_OR_ABORT(py::value_error, "attribute \"{}\" not found", name);
+    }
     #endif
 
 private:
diff --git a/python_binding/backend/pybind_OperatorImpl.cpp b/python_binding/backend/pybind_OperatorImpl.cpp
index 34610069079ee792ebbe4b261b57177b3bbe2997..6a83805fc1af2e111dd1c9f49c669e0c2f9422aa 100644
--- a/python_binding/backend/pybind_OperatorImpl.cpp
+++ b/python_binding/backend/pybind_OperatorImpl.cpp
@@ -11,6 +11,7 @@
 
 #include <pybind11/pybind11.h>
 #include <pybind11/stl.h>
+#include <string>
 
 #include "aidge/operator/Operator.hpp"
 #include "aidge/backend/OperatorImpl.hpp"
@@ -42,18 +43,18 @@ public:
 
         );
     }
-    NbElts_t getNbRequiredData(const IOIndex_t inputIdx) const override {
+    Elts_t getNbRequiredData(const IOIndex_t inputIdx) const override {
         PYBIND11_OVERRIDE_NAME(
-            NbElts_t,
+            Elts_t,
             OperatorImpl,
             "get_nb_required_data",
             getNbRequiredData,
             inputIdx
         );
     }
-    NbElts_t getNbRequiredProtected(const IOIndex_t inputIdx) const override {
+    Elts_t getNbRequiredProtected(const IOIndex_t inputIdx) const override {
         PYBIND11_OVERRIDE_NAME(
-            NbElts_t,
+            Elts_t,
             OperatorImpl,
             "get_nb_required_protected",
             getNbRequiredProtected,
@@ -61,10 +62,10 @@ public:
 
         );
     }
-    NbElts_t getRequiredMemory(const IOIndex_t outputIdx,
+    Elts_t getRequiredMemory(const IOIndex_t outputIdx,
     const std::vector<DimSize_t> &inputsSize) const override {
         PYBIND11_OVERRIDE_NAME(
-            NbElts_t,
+            Elts_t,
             OperatorImpl,
             "get_required_memory",
             getRequiredMemory,
@@ -73,9 +74,9 @@ public:
 
         );
     }
-    NbElts_t getNbConsumedData(const IOIndex_t inputIdx) const override {
+    Elts_t getNbConsumedData(const IOIndex_t inputIdx) const override {
         PYBIND11_OVERRIDE_NAME(
-            NbElts_t,
+            Elts_t,
             OperatorImpl,
             "get_nb_consumed_data",
             getNbConsumedData,
@@ -83,9 +84,9 @@ public:
 
         );
     }
-    NbElts_t getNbProducedData(const IOIndex_t outputIdx) const override {
+    Elts_t getNbProducedData(const IOIndex_t outputIdx) const override {
         PYBIND11_OVERRIDE_NAME(
-            NbElts_t,
+            Elts_t,
             OperatorImpl,
             "get_nb_produced_data",
             getNbProducedData,
@@ -102,12 +103,21 @@ public:
 
         );
     }
+    void resetConsummerProducer() override {
+        PYBIND11_OVERRIDE_NAME(
+            void,
+            OperatorImpl,
+            "reset_consummer_producer",
+            resetConsummerProducer,
+
+        );
+    }
 };
 
 void init_OperatorImpl(py::module& m){
 
     py::class_<OperatorImpl, std::shared_ptr<OperatorImpl>, pyOperatorImpl>(m, "OperatorImpl", py::dynamic_attr())
-    .def(py::init<const Operator&>())
+    .def(py::init<const Operator&, const std::string&>(), py::keep_alive<1, 1>(), py::keep_alive<1, 2>(), py::keep_alive<1,3>())
     .def("forward", &OperatorImpl::forward)
     .def("backward", &OperatorImpl::backward)
     .def("get_nb_required_data", &OperatorImpl::getNbRequiredData)
@@ -116,6 +126,7 @@ void init_OperatorImpl(py::module& m){
     .def("get_nb_consumed_data", &OperatorImpl::getNbConsumedData)
     .def("get_nb_produced_data", &OperatorImpl::getNbProducedData)
     .def("update_consummer_producer", &OperatorImpl::updateConsummerProducer)
+    .def("reset_consummer_producer", &OperatorImpl::resetConsummerProducer)
     ;
 }
 }
diff --git a/python_binding/data/pybind_Data.cpp b/python_binding/data/pybind_Data.cpp
index 3e9f015250acda34f0ae55af38f67df3ca4ad180..bca246c94434b280a12d070526ad4ffb2c7fbe7b 100644
--- a/python_binding/data/pybind_Data.cpp
+++ b/python_binding/data/pybind_Data.cpp
@@ -26,12 +26,11 @@ void init_Data(py::module& m){
     .value("Int64", DataType::Int64)
     .value("UInt8", DataType::UInt8)
     .value("UInt32", DataType::UInt32)
-    .value("UInt64", DataType::UInt64)   
+    .value("UInt64", DataType::UInt64)
     ;
 
-    py::class_<Data, std::shared_ptr<Data>>(m,"Data")
-    .def(py::init<const char*>());
+    py::class_<Data, std::shared_ptr<Data>>(m,"Data");
+
 
-    
 }
 }
diff --git a/python_binding/data/pybind_DataProvider.cpp b/python_binding/data/pybind_DataProvider.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2f652aff5008f8008952ffb1bb6fb1738021b436
--- /dev/null
+++ b/python_binding/data/pybind_DataProvider.cpp
@@ -0,0 +1,36 @@
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+#include "aidge/data/DataProvider.hpp"
+#include "aidge/data/Database.hpp"
+
+namespace py = pybind11;
+
+namespace Aidge {
+
+// __iter__ method for iterator protocol
+DataProvider* DataProvider::iter(){
+    resetIndexBatch();
+    setBatches();
+    return this;
+}
+
+// __next__ method for iterator protocol
+std::vector<std::shared_ptr<Aidge::Tensor>> DataProvider::next() {
+    if (!done()){
+        incrementIndexBatch();
+        return readBatch();
+    } else {
+        throw py::stop_iteration();
+    }
+}
+
+void init_DataProvider(py::module& m){
+
+    py::class_<DataProvider, std::shared_ptr<DataProvider>>(m, "DataProvider")
+        .def(py::init<Database&, std::size_t, bool, bool>(), py::arg("database"), py::arg("batch_size"), py::arg("shuffle"), py::arg("drop_last"))
+        .def("__iter__", &DataProvider::iter)
+        .def("__next__", &DataProvider::next)
+        .def("__len__", &DataProvider::getNbBatch);
+    
+}
+}
diff --git a/python_binding/data/pybind_Database.cpp b/python_binding/data/pybind_Database.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..903e692ca3d14d6ae25f0d6f151b1b08d557d924
--- /dev/null
+++ b/python_binding/data/pybind_Database.cpp
@@ -0,0 +1,13 @@
+#include <pybind11/pybind11.h>
+#include "aidge/data/Database.hpp"
+
+namespace py = pybind11;
+namespace Aidge {
+
+void init_Database(py::module& m){
+
+    py::class_<Database, std::shared_ptr<Database>>(m,"Database");
+
+    
+}
+}
diff --git a/python_binding/data/pybind_Tensor.cpp b/python_binding/data/pybind_Tensor.cpp
index 688a519e593dcde1fe69e3324c81163250eeb42b..b97af94ad583cf42e25fa3afc0697021f6dcadcc 100644
--- a/python_binding/data/pybind_Tensor.cpp
+++ b/python_binding/data/pybind_Tensor.cpp
@@ -30,25 +30,27 @@ void addCtor(py::class_<Tensor,
                         Data,
                         Registrable<Tensor,
                                     std::tuple<std::string, DataType>,
-                                    std::unique_ptr<TensorImpl>(const Tensor&)>>& mTensor){
-    mTensor.def(py::init([]( py::array_t<T, py::array::c_style | py::array::forcecast> b) {
+                                    std::shared_ptr<TensorImpl>(DeviceIdx_t device, std::vector<DimSize_t> dims)>>& mTensor){
+    mTensor.def(py::init([](
+        py::array_t<T, py::array::c_style | py::array::forcecast> b,
+        std::string backend = "cpu") {
         /* Request a buffer descriptor from Python */
         py::buffer_info info = b.request();
         Tensor* newTensor = new Tensor();
         newTensor->setDataType(NativeType<T>::type);
         const std::vector<DimSize_t> dims(info.shape.begin(), info.shape.end());
         newTensor->resize(dims);
-        // TODO : Find a better way to choose backend
+
         std::set<std::string> availableBackends = Tensor::getAvailableBackends();
-        if (availableBackends.find("cpu") != availableBackends.end()){
-            newTensor->setBackend("cpu");
+        if (availableBackends.find(backend) != availableBackends.end()){
+            newTensor->setBackend(backend);
             newTensor->getImpl()->copyFromHost(static_cast<T*>(info.ptr), newTensor->size());
         }else{
-            printf("Warning : Could not use aidge_cpu backend, verify you have `import aidge_cpu`\n");
+            AIDGE_THROW_OR_ABORT(py::value_error, "Could not find backend {}, verify you have `import aidge_backend_{}`.\n", backend, backend);
         }
 
         return newTensor;
-    }))
+    }), py::arg("array"), py::arg("backend")="cpu")
     .def("__setitem__", (void (Tensor::*)(std::size_t, T)) &Tensor::set)
     .def("__setitem__", (void (Tensor::*)(std::vector<std::size_t>, T)) &Tensor::set)
     ;
@@ -58,25 +60,26 @@ void addCtor(py::class_<Tensor,
 void init_Tensor(py::module& m){
     py::class_<Registrable<Tensor,
                            std::tuple<std::string, DataType>,
-                           std::unique_ptr<TensorImpl>(const Tensor&)>,
+                           std::shared_ptr<TensorImpl>(DeviceIdx_t device, std::vector<DimSize_t> dims)>,
                std::shared_ptr<Registrable<Tensor,
                                            std::tuple<std::string, DataType>,
-                                           std::unique_ptr<TensorImpl>(const Tensor&)>>>(m,"TensorRegistrable");
+                                           std::shared_ptr<TensorImpl>(DeviceIdx_t device, std::vector<DimSize_t> dims)>>>(m,"TensorRegistrable");
 
     py::class_<Tensor, std::shared_ptr<Tensor>,
                Data,
                Registrable<Tensor,
                            std::tuple<std::string, DataType>,
-                           std::unique_ptr<TensorImpl>(const Tensor&)>> pyClassTensor
+                           std::shared_ptr<TensorImpl>(DeviceIdx_t device, std::vector<DimSize_t> dims)>> pyClassTensor
         (m,"Tensor", py::multiple_inheritance(), py::buffer_protocol());
 
     pyClassTensor.def(py::init<>())
     .def("set_datatype", &Tensor::setDataType, py::arg("datatype"), py::arg("copyCast") = true)
     .def("set_backend", &Tensor::setBackend, py::arg("name"), py::arg("device") = 0, py::arg("copyFrom") = true)
     .def("dims", (const std::vector<DimSize_t>& (Tensor::*)()const) &Tensor::dims)
+    .def("grad", &Tensor::grad)
     .def("dtype", &Tensor::dataType)
     .def("size", &Tensor::size)
-    .def("resize", (void (Tensor::*)(const std::vector<DimSize_t>&)) &Tensor::resize)
+    .def("resize", (void (Tensor::*)(const std::vector<DimSize_t>&, std::vector<DimSize_t>)) &Tensor::resize)
     .def("has_impl", &Tensor::hasImpl)
     .def("get_coord", &Tensor::getCoord)
     .def("get_idx", &Tensor::getIdx)
@@ -94,10 +97,18 @@ void init_Tensor(py::module& m){
                 return py::cast(b.get<double>(idx));
             case DataType::Float32:
                 return py::cast(b.get<float>(idx));
+            case DataType::Int8:
+                return py::cast(b.get<std::int8_t>(idx));
+            case DataType::Int16:
+                return py::cast(b.get<std::int16_t>(idx));
             case DataType::Int32:
                 return py::cast(b.get<std::int32_t>(idx));
             case DataType::Int64:
                 return py::cast(b.get<std::int64_t>(idx));
+            case DataType::UInt8:
+                return py::cast(b.get<std::uint8_t>(idx));
+            case DataType::UInt16:
+                return py::cast(b.get<std::uint16_t>(idx));
             default:
                 return py::none();
         }
@@ -109,16 +120,24 @@ void init_Tensor(py::module& m){
                 return py::cast(b.get<double>(coordIdx));
             case DataType::Float32:
                 return py::cast(b.get<float>(coordIdx));
+            case DataType::Int8:
+                return py::cast(b.get<std::int8_t>(coordIdx));
+            case DataType::Int16:
+                return py::cast(b.get<std::int16_t>(coordIdx));
             case DataType::Int32:
                 return py::cast(b.get<std::int32_t>(coordIdx));
             case DataType::Int64:
                 return py::cast(b.get<std::int64_t>(coordIdx));
+            case DataType::UInt8:
+                return py::cast(b.get<std::uint8_t>(coordIdx));
+            case DataType::UInt16:
+                return py::cast(b.get<std::uint16_t>(coordIdx));
             default:
                 return py::none();
         }
     })
     .def_buffer([](Tensor& b) -> py::buffer_info {
-        const std::unique_ptr<TensorImpl>& tensorImpl = b.getImpl();
+        const std::shared_ptr<TensorImpl>& tensorImpl = b.getImpl();
 
         std::vector<size_t> dims;
         std::vector<size_t> strides;
@@ -139,6 +158,12 @@ void init_Tensor(py::module& m){
                 break;
             case DataType::Float32:
                 dataFormatDescriptor = py::format_descriptor<float>::format();
+                break;;
+            case DataType::Int8:
+                dataFormatDescriptor = py::format_descriptor<std::int8_t>::format();
+                break;
+            case DataType::Int16:
+                dataFormatDescriptor = py::format_descriptor<std::int16_t>::format();
                 break;
             case DataType::Int32:
                 dataFormatDescriptor = py::format_descriptor<std::int32_t>::format();
@@ -146,6 +171,12 @@ void init_Tensor(py::module& m){
             case DataType::Int64:
                 dataFormatDescriptor = py::format_descriptor<std::int64_t>::format();
                 break;
+            case DataType::UInt8:
+                dataFormatDescriptor = py::format_descriptor<std::uint8_t>::format();
+                break;
+            case DataType::UInt16:
+                dataFormatDescriptor = py::format_descriptor<std::uint16_t>::format();
+                break;
             default:
                 throw py::value_error("Unsupported data format");
         }
diff --git a/python_binding/filler/pybind_Filler.cpp b/python_binding/filler/pybind_Filler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a85c0d6cd6fa0367dfc26328d214c99a4288a3be
--- /dev/null
+++ b/python_binding/filler/pybind_Filler.cpp
@@ -0,0 +1,147 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <pybind11/pybind11.h>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/filler/Filler.hpp"
+
+namespace py = pybind11;
+
+namespace Aidge {
+
+void init_Filler(py::module &m) {
+    py::enum_<enum VarianceNorm>(m, "VarianceNorm")
+        .value("FanIn", VarianceNorm::FanIn)
+        .value("Average", VarianceNorm::Average)
+        .value("FanOut", VarianceNorm::FanOut)
+        .export_values();
+
+    m.def(
+         "constant_filler",
+         [](std::shared_ptr<Tensor> tensor, py::object value) -> void {
+             switch (tensor->dataType()) {
+                 case DataType::Float64:
+                     constantFiller<double>(tensor, value.cast<double>());
+                     break;
+                 case DataType::Float32:
+                     constantFiller<float>(tensor, value.cast<float>());
+                     break;
+                 default:
+                     AIDGE_THROW_OR_ABORT(
+                         py::value_error,
+                         "Data type is not supported for Constant filler.");
+             }
+         },
+         py::arg("tensor"), py::arg("value"))
+        .def(
+            "normal_filler",
+            [](std::shared_ptr<Tensor> tensor, double mean,
+               double stdDev) -> void {
+                switch (tensor->dataType()) {
+                    case DataType::Float64:
+                        normalFiller<double>(tensor, mean, stdDev);
+                        break;
+                    case DataType::Float32:
+                        normalFiller<float>(tensor, mean, stdDev);
+                        break;
+                    default:
+                        AIDGE_THROW_OR_ABORT(
+                            py::value_error,
+                            "Data type is not supported for Normal filler.");
+                }
+            },
+            py::arg("tensor"), py::arg("mean") = 0.0, py::arg("stdDev") = 1.0)
+        .def(
+            "uniform_filler",
+            [](std::shared_ptr<Tensor> tensor, double min, double max) -> void {
+                switch (tensor->dataType()) {
+                    case DataType::Float64:
+                        uniformFiller<double>(tensor, min, max);
+                        break;
+                    case DataType::Float32:
+                        uniformFiller<float>(tensor, min, max);
+                        break;
+                    default:
+                        AIDGE_THROW_OR_ABORT(
+                            py::value_error,
+                            "Data type is not supported for Uniform filler.");
+                }
+            },
+            py::arg("tensor"), py::arg("min"), py::arg("max"))
+        .def(
+            "xavier_uniform_filler",
+            [](std::shared_ptr<Tensor> tensor, py::object scaling,
+               VarianceNorm varianceNorm) -> void {
+                switch (tensor->dataType()) {
+                    case DataType::Float64:
+                        xavierUniformFiller<double>(
+                            tensor, scaling.cast<double>(), varianceNorm);
+                        break;
+                    case DataType::Float32:
+                        xavierUniformFiller<float>(
+                            tensor, scaling.cast<float>(), varianceNorm);
+                        break;
+                    default:
+                        AIDGE_THROW_OR_ABORT(
+                            py::value_error,
+                            "Data type is not supported for Uniform filler.");
+                }
+            },
+            py::arg("tensor"), py::arg("scaling") = 1.0,
+            py::arg("varianceNorm") = VarianceNorm::FanIn)
+        .def(
+            "xavier_normal_filler",
+            [](std::shared_ptr<Tensor> tensor, py::object scaling,
+               VarianceNorm varianceNorm) -> void {
+                switch (tensor->dataType()) {
+                    case DataType::Float64:
+                        xavierNormalFiller<double>(
+                            tensor, scaling.cast<double>(), varianceNorm);
+                        break;
+                    case DataType::Float32:
+                        xavierNormalFiller<float>(tensor, scaling.cast<float>(),
+                                                  varianceNorm);
+                        break;
+                    default:
+                        AIDGE_THROW_OR_ABORT(
+                            py::value_error,
+                            "Data type is not supported for Uniform filler.");
+                }
+            },
+            py::arg("tensor"), py::arg("scaling") = 1.0,
+            py::arg("varianceNorm") = VarianceNorm::FanIn)
+        .def(
+            "he_filler",
+            [](std::shared_ptr<Tensor> tensor, VarianceNorm varianceNorm,
+               py::object meanNorm, py::object scaling) -> void {
+                switch (tensor->dataType()) {
+                    case DataType::Float64:
+                        heFiller<double>(tensor, varianceNorm,
+                                         meanNorm.cast<double>(),
+                                         scaling.cast<double>());
+                        break;
+                    case DataType::Float32:
+                        heFiller<float>(tensor, varianceNorm,
+                                        meanNorm.cast<float>(),
+                                        scaling.cast<float>());
+                        break;
+                    default:
+                        AIDGE_THROW_OR_ABORT(
+                            py::value_error,
+                            "Data type is not supported for Uniform filler.");
+                }
+            },
+            py::arg("tensor"), py::arg("varianceNorm") = VarianceNorm::FanIn,
+            py::arg("meanNorm") = 0.0, py::arg("scaling") = 1.0)
+        ;
+}
+}  // namespace Aidge
diff --git a/python_binding/graph/pybind_GraphView.cpp b/python_binding/graph/pybind_GraphView.cpp
index eb26538a5db1eb40fdcb8a2e409067483d4a7d68..953ec981e06e8c4050ca24143ff832e9f7112f70 100644
--- a/python_binding/graph/pybind_GraphView.cpp
+++ b/python_binding/graph/pybind_GraphView.cpp
@@ -30,7 +30,9 @@ void init_GraphView(py::module& m) {
           :param path: save location
           :type path: str
           )mydelimiter")
-
+          .def("log_outputs", &GraphView::logOutputs, py::arg("path"))
+          .def("get_ordered_inputs", &GraphView::getOrderedInputs)
+          .def("get_ordered_outputs", &GraphView::getOrderedOutputs)
           .def("get_output_nodes", &GraphView::outputNodes,
           R"mydelimiter(
           Get set of output Nodes.
@@ -45,6 +47,9 @@ void init_GraphView(py::module& m) {
           :rtype: list[Node]
           )mydelimiter")
 
+          .def("set_ordered_inputs", &GraphView::setOrderedInputs, py::arg("inputs"))
+          .def("set_ordered_outputs", &GraphView::setOrderedOutputs, py::arg("outputs"))
+
           .def("add", (void (GraphView::*)(std::shared_ptr<Node>, bool)) & GraphView::add,
                py::arg("other_node"), py::arg("include_learnable_parameters") = true,
           R"mydelimiter(
@@ -86,7 +91,19 @@ void init_GraphView(py::module& m) {
           :type to_tensor: int
           )mydelimiter")
 
-          .def_static("replace", &GraphView::replace, py::arg("old_nodes"), py::arg("new_nodes"),
+          .def_static("replace", py::overload_cast<const std::shared_ptr<GraphView>&, const std::shared_ptr<GraphView>&>(&GraphView::replace), py::arg("old_graph"), py::arg("new_graph"),
+          R"mydelimiter(
+          Replace the old set of Nodes in a GraphView with the new set of given Nodes in a GraphView if possible in every GraphView.
+
+          :param old_graph: GraphView of Nodes actually connected in GraphViews.
+          :type old_graph: GraphView
+          :param new_graph: GraphView of Nodes with inner connections already taken care of.
+          :type new_graph: GraphView
+          :return: Whether any replacement has been made.
+          :rtype: bool
+          )mydelimiter")
+
+          .def_static("replace", py::overload_cast<const std::set<NodePtr>&, const std::set<NodePtr>&>(&GraphView::replace), py::arg("old_nodes"), py::arg("new_nodes"),
           R"mydelimiter(
           Replace the old set of Nodes with the new set of given Nodes if possible in every GraphView.
 
@@ -100,8 +117,8 @@ void init_GraphView(py::module& m) {
 
           .def("get_nodes", &GraphView::getNodes)
           .def("get_node", &GraphView::getNode, py::arg("node_name"))
-          .def("forward_dims", &GraphView::forwardDims)
-          .def("compile", &GraphView::compile, py::arg("backend"), py::arg("datatype"), py::arg("device") = 0)
+          .def("forward_dims", &GraphView::forwardDims, py::arg("dims")=std::vector<std::vector<DimSize_t>>())
+          .def("compile", &GraphView::compile, py::arg("backend"), py::arg("datatype"), py::arg("device") = 0, py::arg("dims")=std::vector<std::vector<DimSize_t>>())
           .def("__call__", &GraphView::operator(), py::arg("connectors"))
           .def("set_datatype", &GraphView::setDataType, py::arg("datatype"))
           .def("set_backend", &GraphView::setBackend, py::arg("backend"), py::arg("device") = 0)
@@ -118,5 +135,7 @@ void init_GraphView(py::module& m) {
           //           }
           //      })
             ;
+
+     m.def("get_connected_graph_view", &getConnectedGraphView);
 }
 }  // namespace Aidge
diff --git a/python_binding/graph/pybind_Node.cpp b/python_binding/graph/pybind_Node.cpp
index 83f5688fa3d9e459a364ee3e74975a23d09c236c..116b9dc404861bfba813c4961db8b6c457fae154 100644
--- a/python_binding/graph/pybind_Node.cpp
+++ b/python_binding/graph/pybind_Node.cpp
@@ -23,6 +23,7 @@ namespace py = pybind11;
 namespace Aidge {
 void init_Node(py::module& m) {
     py::class_<Node, std::shared_ptr<Node>>(m, "Node")
+    .def(py::init<std::shared_ptr<Operator>, const std::string&>(), py::arg("op"), py::arg("name") = "")
     .def("name", &Node::name,
     R"mydelimiter(
     Name of the Node.
@@ -56,9 +57,9 @@ void init_Node(py::module& m) {
 
     :param other_node: Pointer to the other Node.
     :type other_node: :py:class: Node
-    :param out_id: ID of the current Node output to connect to the other Node. Default to 0.
+    :param out_id: ID of the output of the current Node to connect to the other Node. (If Node has 1 output max ID is 0). Default to 0.
     :type out_id: int
-    :param other_in_id: ID of the other Node input to connect to the current Node. Default to the first avaible data input.
+    :param other_in_id: ID of the input of the other Node to connect to the current Node (If the node is a Mul op it has 2 input then Max ID is 1).Default to the first avaible data input.
     :type other_in_id: int
     )mydelimiter")
 
@@ -84,7 +85,7 @@ void init_Node(py::module& m) {
     :type other_view: :py:class: GraphView
     :param out_id: ID of the current Node output to connect to the other Node. Default to 0.
     :type out_id: int
-    :param other_in_id: Pair of Node and input connection ID for specifying the connection. If the GraphView whose content is linked has only one input Node,   then it defaults to the first available data input ID of this Node.
+    :param other_in_id: Pair of Node and input connection ID for specifying the connection. If the GraphView whose content is linked has only one input Node, then it defaults to the first available data input ID of this Node.
     :type other_in_id: tuple[:py:class: Node, int]
     )mydelimiter")
 
diff --git a/python_binding/operator/pybind_Add.cpp b/python_binding/operator/pybind_Add.cpp
index 74ec11c28e746856fe767f16a4380651271d8fe4..c3eeb192a88163be96f973a55e6ef7cc60ec48af 100644
--- a/python_binding/operator/pybind_Add.cpp
+++ b/python_binding/operator/pybind_Add.cpp
@@ -12,6 +12,7 @@
 #include <pybind11/pybind11.h>
 
 #include "aidge/operator/Add.hpp"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/backend/OperatorImpl.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/utils/Types.h"
@@ -23,7 +24,7 @@ void declare_Add(py::module &m) {
   py::class_<Add_Op, std::shared_ptr<Add_Op>, OperatorTensor>(m, "AddOp", py::multiple_inheritance())
   .def("get_inputs_name", &Add_Op::getInputsName)
   .def("get_outputs_name", &Add_Op::getOutputsName);
-
+  declare_registrable<Add_Op>(m, "AddOp");
   m.def("Add", &Add, py::arg("nbIn"), py::arg("name") = "");
 }
 
diff --git a/python_binding/operator/pybind_AvgPooling.cpp b/python_binding/operator/pybind_AvgPooling.cpp
index f87cd5dd66f44535ff895f73b160fc5988e1009a..5d72a3507c4926412b48cda42b1c3bcbe10e9460 100644
--- a/python_binding/operator/pybind_AvgPooling.cpp
+++ b/python_binding/operator/pybind_AvgPooling.cpp
@@ -9,38 +9,40 @@
  *
  ********************************************************************************/
 
-#include <pybind11/pybind11.h>
-#include <pybind11/stl.h>
-
+#include <array>
 #include <string>
 #include <vector>
-#include <array>
+
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
 
 #include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/AvgPooling.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/utils/Types.h"
-#include "aidge/data/Tensor.hpp"
 
 namespace py = pybind11;
 namespace Aidge {
 
 template <DimIdx_t DIM> void declare_AvgPoolingOp(py::module &m) {
-  py::class_<AvgPooling_Op<DIM>, std::shared_ptr<AvgPooling_Op<DIM>>, OperatorTensor, Attributes>(
-    m, ("AvgPoolingOp" + std::to_string(DIM) + "D").c_str(),
+  const std::string pyClassName("AvgPoolingOp" + std::to_string(DIM) + "D");
+  py::class_<AvgPooling_Op<DIM>, std::shared_ptr<AvgPooling_Op<DIM>>, Attributes, OperatorTensor>(
+    m, pyClassName.c_str(),
     py::multiple_inheritance())
   .def(py::init<const std::array<DimSize_t, DIM> &,
                 const std::array<DimSize_t, DIM> &>(),
         py::arg("kernel_dims"),
         py::arg("stride_dims"))
   .def("get_inputs_name", &AvgPooling_Op<DIM>::getInputsName)
-  .def("get_outputs_name", &AvgPooling_Op<DIM>::getOutputsName);
-
+  .def("get_outputs_name", &AvgPooling_Op<DIM>::getOutputsName)
+  .def("attributes_name", &AvgPooling_Op<DIM>::staticGetAttrsName);
+  declare_registrable<AvgPooling_Op<DIM>>(m, pyClassName);
   m.def(("AvgPooling" + std::to_string(DIM) + "D").c_str(), [](const std::vector<DimSize_t>& kernel_dims,
                                                                   const std::string& name,
                                                                   const std::vector<DimSize_t> &stride_dims) {
-        AIDGE_ASSERT(kernel_dims.size() == DIM, "kernel_dims size [%ld] does not match DIM [%d]", kernel_dims.size(), DIM);
-        AIDGE_ASSERT(stride_dims.size() == DIM, "stride_dims size [%ld] does not match DIM [%d]", stride_dims.size(), DIM);
+        AIDGE_ASSERT(kernel_dims.size() == DIM, "kernel_dims size [{}] does not match DIM [{}]", kernel_dims.size(), DIM);
+        AIDGE_ASSERT(stride_dims.size() == DIM, "stride_dims size [{}] does not match DIM [{}]", stride_dims.size(), DIM);
 
         return AvgPooling<DIM>(to_array<DIM>(kernel_dims.begin()), name, to_array<DIM>(stride_dims.begin()));
     }, py::arg("kernel_dims"),
diff --git a/python_binding/operator/pybind_BatchNorm.cpp b/python_binding/operator/pybind_BatchNorm.cpp
index 411a2e1b6ae78065a79b92f25c23dac13e341997..9640141e03bcd811f5ce24c544c5cdbc9fe6b2f3 100644
--- a/python_binding/operator/pybind_BatchNorm.cpp
+++ b/python_binding/operator/pybind_BatchNorm.cpp
@@ -12,6 +12,7 @@
 #include <pybind11/pybind11.h>
 #include <string>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/BatchNorm.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/utils/Types.h"
@@ -21,9 +22,15 @@ namespace Aidge {
 
 template <DimSize_t DIM>
 void declare_BatchNormOp(py::module& m) {
-    py::class_<BatchNorm_Op<DIM>, std::shared_ptr<BatchNorm_Op<DIM>>, OperatorTensor, Attributes>(m, ("BatchNormOp" + std::to_string(DIM) + "D").c_str(), py::multiple_inheritance())
+    const std::string pyClassName("BatchNormOp" + std::to_string(DIM) + "D");
+    py::class_<BatchNorm_Op<DIM>, std::shared_ptr<BatchNorm_Op<DIM>>, Attributes, OperatorTensor>(m, pyClassName.c_str(), py::multiple_inheritance())
+    .def(py::init<float, float>(),
+        py::arg("epsilon"),
+        py::arg("momentum"))
     .def("get_inputs_name", &BatchNorm_Op<DIM>::getInputsName)
-    .def("get_outputs_name", &BatchNorm_Op<DIM>::getOutputsName);
+    .def("get_outputs_name", &BatchNorm_Op<DIM>::getOutputsName)
+    .def("attributes_name", &BatchNorm_Op<DIM>::staticGetAttrsName);
+    declare_registrable<BatchNorm_Op<DIM>>(m, pyClassName);
 
     m.def(("BatchNorm" + std::to_string(DIM) + "D").c_str(), &BatchNorm<DIM>, py::arg("nbFeatures"), py::arg("epsilon") = 1.0e-5F, py::arg("momentum") = 0.1F, py::arg("name") = "");
 }
diff --git a/python_binding/operator/pybind_Concat.cpp b/python_binding/operator/pybind_Concat.cpp
index 2b7e5d6b99194e914e48dc6263d0bdcd6a4a8a2f..756686c209c33fe03f7bda4bbb53d8c3c71e8b4c 100644
--- a/python_binding/operator/pybind_Concat.cpp
+++ b/python_binding/operator/pybind_Concat.cpp
@@ -12,6 +12,7 @@
 #include <pybind11/pybind11.h>
 #include <string>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Concat.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 
@@ -19,10 +20,12 @@ namespace py = pybind11;
 namespace Aidge {
 
 void init_Concat(py::module& m) {
-    py::class_<Concat_Op, std::shared_ptr<Concat_Op>, OperatorTensor, Attributes>(m, "ConcatOp", py::multiple_inheritance())
+    py::class_<Concat_Op, std::shared_ptr<Concat_Op>, Attributes, OperatorTensor>(m, "ConcatOp", py::multiple_inheritance())
     .def("get_inputs_name", &Concat_Op::getInputsName)
-    .def("get_outputs_name", &Concat_Op::getOutputsName);
+    .def("get_outputs_name", &Concat_Op::getOutputsName)
+    .def("attributes_name", &Concat_Op::staticGetAttrsName);
 
+    declare_registrable<Concat_Op>(m, "ConcatOp");
     m.def("Concat", &Concat, py::arg("nbIn"), py::arg("axis"), py::arg("name") = "");
 }
 }  // namespace Aidge
diff --git a/python_binding/operator/pybind_Conv.cpp b/python_binding/operator/pybind_Conv.cpp
index 2200cd3fec1450011d6e0b5197f8b99b4dfeb4c3..adb0e108c409032c7e132016f5b92ed9f9233491 100644
--- a/python_binding/operator/pybind_Conv.cpp
+++ b/python_binding/operator/pybind_Conv.cpp
@@ -16,48 +16,58 @@
 #include <array>
 
 #include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Conv.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/utils/Types.h"
+#include "aidge/utils/Registrar.hpp" // declare_registrable
 
 namespace py = pybind11;
 namespace Aidge {
 
 template <DimIdx_t DIM> void declare_ConvOp(py::module &m) {
-  py::class_<Conv_Op<DIM>, std::shared_ptr<Conv_Op<DIM>>, OperatorTensor, Attributes>(
-    m, ("ConvOp" + std::to_string(DIM) + "D").c_str(),
+  const std::string pyClassName("ConvOp" + std::to_string(DIM) + "D");
+  py::class_<Conv_Op<DIM>, std::shared_ptr<Conv_Op<DIM>>, Attributes, OperatorTensor>(
+    m, pyClassName.c_str(),
     py::multiple_inheritance())
   .def(py::init<DimSize_t,
                 DimSize_t,
                 const std::array<DimSize_t, DIM> &,
                 const std::array<DimSize_t, DIM> &,
-                const std::array<DimSize_t, DIM> &>(),
+                const std::array<DimSize_t, DIM> &,
+                bool>(),
         py::arg("in_channels"),
         py::arg("out_channels"),
         py::arg("kernel_dims"),
         py::arg("stride_dims"),
-        py::arg("dilation_dims"))
+        py::arg("dilation_dims"),
+        py::arg("no_bias"))
     .def("get_inputs_name", &Conv_Op<DIM>::getInputsName)
     .def("get_outputs_name", &Conv_Op<DIM>::getOutputsName)
+    .def("attributes_name", &Conv_Op<DIM>::staticGetAttrsName)
     ;
+  declare_registrable<Conv_Op<DIM>>(m, pyClassName);
+
 
   m.def(("Conv" + std::to_string(DIM) + "D").c_str(), [](DimSize_t in_channels,
                                                          DimSize_t out_channels,
                                                          const std::vector<DimSize_t>& kernel_dims,
                                                          const std::string& name,
                                                          const std::vector<DimSize_t> &stride_dims,
-                                                         const std::vector<DimSize_t> &dilation_dims) {
-        AIDGE_ASSERT(kernel_dims.size() == DIM, "kernel_dims size [%ld] does not match DIM [%d]", kernel_dims.size(), DIM);
-        AIDGE_ASSERT(stride_dims.size() == DIM, "stride_dims size [%ld] does not match DIM [%d]", stride_dims.size(), DIM);
-        AIDGE_ASSERT(dilation_dims.size() == DIM, "dilation_dims size [%ld] does not match DIM [%d]", dilation_dims.size(), DIM);
+                                                         const std::vector<DimSize_t> &dilation_dims,
+                                                         bool noBias) {
+        AIDGE_ASSERT(kernel_dims.size() == DIM, "kernel_dims size [{}] does not match DIM [{}]", kernel_dims.size(), DIM);
+        AIDGE_ASSERT(stride_dims.size() == DIM, "stride_dims size [{}] does not match DIM [{}]", stride_dims.size(), DIM);
+        AIDGE_ASSERT(dilation_dims.size() == DIM, "dilation_dims size [{}] does not match DIM [{}]", dilation_dims.size(), DIM);
 
-        return Conv<DIM>(in_channels, out_channels, to_array<DIM>(kernel_dims.begin()), name, to_array<DIM>(stride_dims.begin()), to_array<DIM>(dilation_dims.begin()));
+        return Conv<DIM>(in_channels, out_channels, to_array<DIM>(kernel_dims.begin()), name, to_array<DIM>(stride_dims.begin()), to_array<DIM>(dilation_dims.begin()), noBias);
     }, py::arg("in_channels"),
        py::arg("out_channels"),
        py::arg("kernel_dims"),
        py::arg("name") = "",
        py::arg("stride_dims") = std::vector<DimSize_t>(DIM,1),
-       py::arg("dilation_dims") = std::vector<DimSize_t>(DIM,1));
+       py::arg("dilation_dims") = std::vector<DimSize_t>(DIM,1),
+       py::arg("no_bias") = false);
 }
 
 
@@ -65,9 +75,5 @@ void init_Conv(py::module &m) {
   declare_ConvOp<1>(m);
   declare_ConvOp<2>(m);
   declare_ConvOp<3>(m);
-
-  // FIXME:
-  // m.def("Conv1D", static_cast<NodeAPI(*)(const char*, int, int, int const
-  // (&)[1])>(&Conv));
 }
 } // namespace Aidge
diff --git a/python_binding/operator/pybind_ConvDepthWise.cpp b/python_binding/operator/pybind_ConvDepthWise.cpp
index 15f2c1c8acb4a1b59cfb0f35ebb78cb611647d3b..19b3332a84037185afdc87fd90cb9c8fea2e64f8 100644
--- a/python_binding/operator/pybind_ConvDepthWise.cpp
+++ b/python_binding/operator/pybind_ConvDepthWise.cpp
@@ -17,6 +17,7 @@
 #include <array>
 
 #include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/ConvDepthWise.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/utils/Types.h"
@@ -26,35 +27,41 @@ namespace py = pybind11;
 namespace Aidge {
 
 template <DimIdx_t DIM> void declare_ConvDepthWiseOp(py::module &m) {
-  py::class_<ConvDepthWise_Op<DIM>, std::shared_ptr<ConvDepthWise_Op<DIM>>, OperatorTensor, Attributes>(
-    m, ("ConvDepthWiseOp" + std::to_string(DIM) + "D").c_str(),
+  const std::string pyClassName("ConvDepthWiseOp" + std::to_string(DIM) + "D");
+  py::class_<ConvDepthWise_Op<DIM>, std::shared_ptr<ConvDepthWise_Op<DIM>>, Attributes, OperatorTensor>(
+    m, pyClassName.c_str(),
     py::multiple_inheritance())
   .def(py::init<const DimSize_t,
                 const std::array<DimSize_t, DIM> &,
                 const std::array<DimSize_t, DIM> &,
-                const std::array<DimSize_t, DIM> &>(),
+                const std::array<DimSize_t, DIM> &,
+                bool>(),
         py::arg("nb_channels"),
         py::arg("kernel_dims"),
         py::arg("stride_dims"),
-        py::arg("dilation_dims"))
+        py::arg("dilation_dims"),
+        py::arg("no_bias"))
   .def("get_inputs_name", &ConvDepthWise_Op<DIM>::getInputsName)
-  .def("get_outputs_name", &ConvDepthWise_Op<DIM>::getOutputsName);
-
+  .def("get_outputs_name", &ConvDepthWise_Op<DIM>::getOutputsName)
+  .def("attributes_name", &ConvDepthWise_Op<DIM>::staticGetAttrsName);
+  declare_registrable<ConvDepthWise_Op<DIM>>(m, pyClassName);
   m.def(("ConvDepthWise" + std::to_string(DIM) + "D").c_str(), [](const DimSize_t nb_channels,
                                                                   const std::vector<DimSize_t>& kernel_dims,
                                                                   const std::string& name,
                                                                   const std::vector<DimSize_t> &stride_dims,
-                                                                  const std::vector<DimSize_t> &dilation_dims) {
-        AIDGE_ASSERT(kernel_dims.size() == DIM, "kernel_dims size [%ld] does not match DIM [%d]", kernel_dims.size(), DIM);
-        AIDGE_ASSERT(stride_dims.size() == DIM, "stride_dims size [%ld] does not match DIM [%d]", stride_dims.size(), DIM);
-        AIDGE_ASSERT(dilation_dims.size() == DIM, "dilation_dims size [%ld] does not match DIM [%d]", dilation_dims.size(), DIM);
+                                                                  const std::vector<DimSize_t> &dilation_dims,
+                                                                  bool no_bias) {
+        AIDGE_ASSERT(kernel_dims.size() == DIM, "kernel_dims size [{}] does not match DIM [{}]", kernel_dims.size(), DIM);
+        AIDGE_ASSERT(stride_dims.size() == DIM, "stride_dims size [{}] does not match DIM [{}]", stride_dims.size(), DIM);
+        AIDGE_ASSERT(dilation_dims.size() == DIM, "dilation_dims size [{}] does not match DIM [{}]", dilation_dims.size(), DIM);
 
-        return ConvDepthWise<DIM>(nb_channels, to_array<DIM>(kernel_dims.begin()), name, to_array<DIM>(stride_dims.begin()), to_array<DIM>(dilation_dims.begin()));
+        return ConvDepthWise<DIM>(nb_channels, to_array<DIM>(kernel_dims.begin()), name, to_array<DIM>(stride_dims.begin()), to_array<DIM>(dilation_dims.begin()), no_bias);
     }, py::arg("nb_channenls"),
        py::arg("kernel_dims"),
        py::arg("name") = "",
        py::arg("stride_dims") = std::vector<DimSize_t>(DIM,1),
-       py::arg("dilation_dims") = std::vector<DimSize_t>(DIM,1));
+       py::arg("dilation_dims") = std::vector<DimSize_t>(DIM,1),
+       py::arg("no_bias")= false);
 
 }
 
diff --git a/python_binding/operator/pybind_Div.cpp b/python_binding/operator/pybind_Div.cpp
index 6d14510f34349c001289096a7fc9b08681a25bc8..e9bf26b629aa05090c9601103676cbc12ff4c88d 100644
--- a/python_binding/operator/pybind_Div.cpp
+++ b/python_binding/operator/pybind_Div.cpp
@@ -11,6 +11,7 @@
 
 #include <pybind11/pybind11.h>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Div.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 
@@ -21,7 +22,7 @@ void init_Div(py::module& m) {
     py::class_<Div_Op, std::shared_ptr<Div_Op>, OperatorTensor>(m, "DivOp", py::multiple_inheritance())
     .def("get_inputs_name", &Div_Op::getInputsName)
     .def("get_outputs_name", &Div_Op::getOutputsName);
-
+    declare_registrable<Div_Op>(m, "DivOp");
     m.def("Div", &Div, py::arg("name") = "");
 }
 }  // namespace Aidge
diff --git a/python_binding/operator/pybind_Erf.cpp b/python_binding/operator/pybind_Erf.cpp
index 806867f61c3580543c184d529edc2856ee8d7a6c..c5fd53f2a665b5b816a3778e6f874cd04956e99e 100644
--- a/python_binding/operator/pybind_Erf.cpp
+++ b/python_binding/operator/pybind_Erf.cpp
@@ -11,6 +11,7 @@
 
 #include <pybind11/pybind11.h>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Erf.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 
@@ -21,7 +22,7 @@ void init_Erf(py::module& m) {
     py::class_<Erf_Op, std::shared_ptr<Erf_Op>, OperatorTensor>(m, "ErfOp", py::multiple_inheritance())
     .def("get_inputs_name", &Erf_Op::getInputsName)
     .def("get_outputs_name", &Erf_Op::getOutputsName);
-
+    declare_registrable<Erf_Op>(m, "ErfOp");
     m.def("Erf", &Erf, py::arg("name") = "");
 }
 }  // namespace Aidge
diff --git a/python_binding/operator/pybind_FC.cpp b/python_binding/operator/pybind_FC.cpp
index 606b9ae948847f98d5a1129c08db21e073311879..ab1ed9ce20bec01e205cd6478c6a93df9f91a2fb 100644
--- a/python_binding/operator/pybind_FC.cpp
+++ b/python_binding/operator/pybind_FC.cpp
@@ -11,8 +11,9 @@
 
 #include <pybind11/pybind11.h>
 
-#include "aidge/operator/FC.hpp"
 #include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/operator/FC.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/utils/Types.h"
 
@@ -20,10 +21,11 @@ namespace py = pybind11;
 namespace Aidge {
 
 void declare_FC(py::module &m) {
-  py::class_<FC_Op, std::shared_ptr<FC_Op>, OperatorTensor, Attributes>(m, "FCOp", py::multiple_inheritance())
+  py::class_<FC_Op, std::shared_ptr<FC_Op>, Attributes, OperatorTensor>(m, "FCOp", py::multiple_inheritance())
   .def("get_inputs_name", &FC_Op::getInputsName)
-  .def("get_outputs_name", &FC_Op::getOutputsName);
-
+  .def("get_outputs_name", &FC_Op::getOutputsName)
+  .def("attributes_name", &FC_Op::staticGetAttrsName);
+  declare_registrable<FC_Op>(m, "FCOp");
   m.def("FC", &FC, py::arg("in_channels"), py::arg("out_channels"), py::arg("nobias") = false, py::arg("name") = "");
 }
 
diff --git a/python_binding/operator/pybind_Gather.cpp b/python_binding/operator/pybind_Gather.cpp
index f9768e38fbdceef4a15cc74430bc2205bb32cb6a..8c32acfe2bd7e0118c186be8fa1297ee16fe6f6c 100644
--- a/python_binding/operator/pybind_Gather.cpp
+++ b/python_binding/operator/pybind_Gather.cpp
@@ -12,6 +12,7 @@
 #include <pybind11/pybind11.h>
 #include <string>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Gather.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 
@@ -19,10 +20,11 @@ namespace py = pybind11;
 namespace Aidge {
 
 void init_Gather(py::module& m) {
-    py::class_<Gather_Op, std::shared_ptr<Gather_Op>, OperatorTensor, Attributes>(m, "GatherOp", py::multiple_inheritance())
+    py::class_<Gather_Op, std::shared_ptr<Gather_Op>, Attributes, OperatorTensor>(m, "GatherOp", py::multiple_inheritance())
     .def("get_inputs_name", &Gather_Op::getInputsName)
-    .def("get_outputs_name", &Gather_Op::getOutputsName);
-
-    m.def("Gather", &Gather, py::arg("axis"), py::arg("name") = "");
+    .def("get_outputs_name", &Gather_Op::getOutputsName)
+    .def("attributes_name", &Gather_Op::staticGetAttrsName);
+    declare_registrable<Gather_Op>(m, "GatherOp");
+    m.def("Gather", &Gather, py::arg("indices"), py::arg("gathered_shape"), py::arg("axis")= 0, py::arg("name") = "");
 }
 }  // namespace Aidge
diff --git a/python_binding/operator/pybind_GenericOperator.cpp b/python_binding/operator/pybind_GenericOperator.cpp
index 154fdfa64f279d8d6bb40ea7077acdb4c0fd51b9..31ee946fc99df40133ff04965c762f9ddae0d131 100644
--- a/python_binding/operator/pybind_GenericOperator.cpp
+++ b/python_binding/operator/pybind_GenericOperator.cpp
@@ -15,19 +15,42 @@
 #include <stdio.h>
 
 #include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/GenericOperator.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 namespace py = pybind11;
 namespace Aidge {
 
 void init_GenericOperator(py::module& m) {
-    py::class_<GenericOperator_Op, std::shared_ptr<GenericOperator_Op>, OperatorTensor, DynamicAttributes>(m, "GenericOperatorOp",
+    py::class_<GenericOperator_Op, std::shared_ptr<GenericOperator_Op>, DynamicAttributes, OperatorTensor>(m, "GenericOperatorOp",
                                                                                   py::multiple_inheritance())
     .def_readonly_static("identity", &GenericOperator_Op::Identity)
-    .def("compute_output_dims", &GenericOperator_Op::computeOutputDims)
     .def("set_compute_output_dims", &GenericOperator_Op::setComputeOutputDims, py::arg("computation_function"));
 
-    m.def("GenericOperator", &GenericOperator, py::arg("type"), py::arg("nb_data"), py::arg("nb_param"), py::arg("nb_out"),
-          py::arg("name") = "");
+    // &GenericOperator
+    m.def("GenericOperator",
+        []( const std::string& type,
+            IOIndex_t nbData,
+            IOIndex_t nbParam,
+            IOIndex_t nbOut,
+            const std::string& name,
+            const py::kwargs kwargs){
+            std::shared_ptr<Node> genericNode = GenericOperator(
+                type,
+                nbData,
+                nbParam,
+                nbOut,
+                name
+            );
+            if (kwargs){
+                std::shared_ptr<GenericOperator_Op> gop = std::static_pointer_cast<GenericOperator_Op>(genericNode->getOperator());
+                for (auto item : kwargs) {
+                    std::string key = py::cast<std::string>(item.first);
+                    py::object value = py::reinterpret_borrow<py::object>(item.second);
+                    gop->setAttrPy(key, std::move(value));
+                }
+            }
+            return genericNode;
+        }, py::arg("type"), py::arg("nb_data"), py::arg("nb_param"), py::arg("nb_out"), py::arg("name") = "");
 }
 }  // namespace Aidge
diff --git a/python_binding/operator/pybind_GlobalAveragePooling.cpp b/python_binding/operator/pybind_GlobalAveragePooling.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..08c8ad6f9c6b742eee9e9b0ad1ac20217d152bda
--- /dev/null
+++ b/python_binding/operator/pybind_GlobalAveragePooling.cpp
@@ -0,0 +1,31 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <pybind11/pybind11.h>
+
+#include "aidge/operator/GlobalAveragePooling.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/utils/Attributes.hpp"
+
+namespace py = pybind11;
+namespace Aidge {
+
+const std::string pyClassName("GlobalAveragePoolingOp");
+void init_GlobalAveragePooling(py::module &m) {
+  py::class_<GlobalAveragePooling_Op, std::shared_ptr<GlobalAveragePooling_Op>,
+             OperatorTensor>(m, pyClassName.c_str(),
+                             py::multiple_inheritance())
+      .def("get_inputs_name", &GlobalAveragePooling_Op::getInputsName)
+      .def("get_outputs_name", &GlobalAveragePooling_Op::getOutputsName);
+  declare_registrable<GlobalAveragePooling_Op>(m, pyClassName);
+  m.def("globalaveragepooling", &GlobalAveragePooling, py::arg("name") = "");
+}
+} // namespace Aidge
diff --git a/python_binding/operator/pybind_Identity.cpp b/python_binding/operator/pybind_Identity.cpp
index b1b1e8888976c578ff490f35776c890ba59911dc..4538b72fcb012a35ca0ebf3a15449a4b5cfff7a8 100644
--- a/python_binding/operator/pybind_Identity.cpp
+++ b/python_binding/operator/pybind_Identity.cpp
@@ -11,6 +11,7 @@
 
 #include <pybind11/pybind11.h>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Identity.hpp"
 #include "aidge/operator/Operator.hpp"
 
diff --git a/python_binding/operator/pybind_LeakyReLU.cpp b/python_binding/operator/pybind_LeakyReLU.cpp
index 07300633ad1fb8163d4456afd744c4eb5d7b0ed1..9ad47e7a391698ae9b30d35d94f05e8b80138590 100644
--- a/python_binding/operator/pybind_LeakyReLU.cpp
+++ b/python_binding/operator/pybind_LeakyReLU.cpp
@@ -11,6 +11,7 @@
 
 #include <pybind11/pybind11.h>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/LeakyReLU.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 
@@ -18,10 +19,11 @@ namespace py = pybind11;
 namespace Aidge {
 
 void init_LeakyReLU(py::module& m) {
-    py::class_<LeakyReLU_Op, std::shared_ptr<LeakyReLU_Op>, OperatorTensor, Attributes>(m, "LeakyReLUOp", py::multiple_inheritance())
+    py::class_<LeakyReLU_Op, std::shared_ptr<LeakyReLU_Op>, Attributes, OperatorTensor>(m, "LeakyReLUOp", py::multiple_inheritance())
     .def("get_inputs_name", &LeakyReLU_Op::getInputsName)
-    .def("get_outputs_name", &LeakyReLU_Op::getOutputsName);
-
+    .def("get_outputs_name", &LeakyReLU_Op::getOutputsName)
+    .def("attributes_name", &LeakyReLU_Op::staticGetAttrsName);
+    declare_registrable<LeakyReLU_Op>(m, "LeakyReLUOp");
     m.def("LeakyReLU", &LeakyReLU, py::arg("negative_slope") = 0.0f, py::arg("name") = "");
 }
 }  // namespace Aidge
diff --git a/python_binding/operator/pybind_Matmul.cpp b/python_binding/operator/pybind_Matmul.cpp
index 242bf2c451723677e1b9063edfc3098d4159e5a4..73bfac04a78ec9b972ec984466dbae582b2c03dc 100644
--- a/python_binding/operator/pybind_Matmul.cpp
+++ b/python_binding/operator/pybind_Matmul.cpp
@@ -11,23 +11,20 @@
 
 #include <pybind11/pybind11.h>
 
-#include "aidge/operator/MatMul.hpp"
 #include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/operator/MatMul.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/utils/Types.h"
 
 namespace py = pybind11;
 namespace Aidge {
 
-void declare_MatMul(py::module &m) {
-  py::class_<MatMul_Op, std::shared_ptr<MatMul_Op>, OperatorTensor, Attributes>(m, "MatMulOp", py::multiple_inheritance())
+void init_MatMul(py::module &m) {
+  py::class_<MatMul_Op, std::shared_ptr<MatMul_Op>, OperatorTensor>(m, "MatMulOp", py::multiple_inheritance())
   .def("get_inputs_name", &MatMul_Op::getInputsName)
   .def("get_outputs_name", &MatMul_Op::getOutputsName);
-
-  m.def("MatMul", &MatMul, py::arg("in_channels"), py::arg("out_channels"), py::arg("name") = "");
-}
-
-void init_MatMul(py::module &m) {
-  declare_MatMul(m);
+  declare_registrable<MatMul_Op>(m, "MatMulOp");
+  m.def("MatMul", &MatMul, py::arg("name") = "");
 }
 } // namespace Aidge
diff --git a/python_binding/operator/pybind_MaxPooling.cpp b/python_binding/operator/pybind_MaxPooling.cpp
index 0ee3d9df80d7ea7b7be2b8d5c456d5d739506882..91fa0489d8bedd16dd33424e33d7e15eea3e3ecb 100644
--- a/python_binding/operator/pybind_MaxPooling.cpp
+++ b/python_binding/operator/pybind_MaxPooling.cpp
@@ -17,16 +17,17 @@
 #include <array>
 
 #include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/MaxPooling.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/utils/Types.h"
-#include "aidge/data/Tensor.hpp"
 
 namespace py = pybind11;
 namespace Aidge {
 
 template <DimIdx_t DIM> void declare_MaxPoolingOp(py::module &m) {
-  py::class_<MaxPooling_Op<DIM>, std::shared_ptr<MaxPooling_Op<DIM>>, OperatorTensor, Attributes>(
+  const std::string pyClassName("MaxPoolingOp" + std::to_string(DIM) + "D");
+  py::class_<MaxPooling_Op<DIM>, std::shared_ptr<MaxPooling_Op<DIM>>, Attributes, OperatorTensor>(
     m, ("MaxPoolingOp" + std::to_string(DIM) + "D").c_str(),
     py::multiple_inheritance())
   .def(py::init<const std::array<DimSize_t, DIM> &,
@@ -36,14 +37,15 @@ template <DimIdx_t DIM> void declare_MaxPoolingOp(py::module &m) {
         py::arg("stride_dims"),
         py::arg("ceil_mode"))
   .def("get_inputs_name", &MaxPooling_Op<DIM>::getInputsName)
-  .def("get_outputs_name", &MaxPooling_Op<DIM>::getOutputsName);
-
+  .def("get_outputs_name", &MaxPooling_Op<DIM>::getOutputsName)
+  .def("attributes_name", &MaxPooling_Op<DIM>::staticGetAttrsName);
+  declare_registrable<MaxPooling_Op<DIM>>(m, pyClassName);
   m.def(("MaxPooling" + std::to_string(DIM) + "D").c_str(), [](const std::vector<DimSize_t>& kernel_dims,
                                                                   const std::string& name,
                                                                   const std::vector<DimSize_t> &stride_dims,
                                                                   bool ceil_mode) {
-        AIDGE_ASSERT(kernel_dims.size() == DIM, "kernel_dims size [%ld] does not match DIM [%d]", kernel_dims.size(), DIM);
-        AIDGE_ASSERT(stride_dims.size() == DIM, "stride_dims size [%ld] does not match DIM [%d]", stride_dims.size(), DIM);
+        AIDGE_ASSERT(kernel_dims.size() == DIM, "kernel_dims size [{}] does not match DIM [{}]", kernel_dims.size(), DIM);
+        AIDGE_ASSERT(stride_dims.size() == DIM, "stride_dims size [{}] does not match DIM [{}]", stride_dims.size(), DIM);
 
         return MaxPooling<DIM>(to_array<DIM>(kernel_dims.begin()), name, to_array<DIM>(stride_dims.begin()), ceil_mode);
     }, py::arg("kernel_dims"),
diff --git a/python_binding/operator/pybind_MetaOperatorDefs.cpp b/python_binding/operator/pybind_MetaOperatorDefs.cpp
index b043ac23c378b9d591b7d1273ebcb5d48a37394a..20cd3f156996c98bb64502a90ab98535f87cc2a3 100644
--- a/python_binding/operator/pybind_MetaOperatorDefs.cpp
+++ b/python_binding/operator/pybind_MetaOperatorDefs.cpp
@@ -30,21 +30,23 @@ template <DimIdx_t DIM> void declare_PaddedConvOp(py::module &m) {
                                                          const std::string& name,
                                                          const std::vector<DimSize_t> &stride_dims,
                                                          const std::vector<DimSize_t> &padding_dims,
-                                                         const std::vector<DimSize_t> &dilation_dims)
+                                                         const std::vector<DimSize_t> &dilation_dims,
+                                                         bool no_bias)
     {
-        AIDGE_ASSERT(kernel_dims.size() == DIM, "kernel_dims size [%ld] does not match DIM [%d]", kernel_dims.size(), DIM);
-        AIDGE_ASSERT(stride_dims.size() == DIM, "stride_dims size [%ld] does not match DIM [%d]", stride_dims.size(), DIM);
-        AIDGE_ASSERT(padding_dims.size() == 2*DIM, "padding_dims size [%ld] does not match DIM [%d]", padding_dims.size(), 2*DIM);
-        AIDGE_ASSERT(dilation_dims.size() == DIM, "dilation_dims size [%ld] does not match DIM [%d]", dilation_dims.size(), DIM);
+        AIDGE_ASSERT(kernel_dims.size() == DIM, "kernel_dims size [{}] does not match DIM [{}]", kernel_dims.size(), DIM);
+        AIDGE_ASSERT(stride_dims.size() == DIM, "stride_dims size [{}] does not match DIM [{}]", stride_dims.size(), DIM);
+        AIDGE_ASSERT(padding_dims.size() == 2*DIM, "padding_dims size [{}] does not match DIM [{}]", padding_dims.size(), 2*DIM);
+        AIDGE_ASSERT(dilation_dims.size() == DIM, "dilation_dims size [{}] does not match DIM [{}]", dilation_dims.size(), DIM);
 
-        return PaddedConv<DIM>(in_channels, out_channels, to_array<DIM>(kernel_dims.begin()), name, to_array<DIM>(stride_dims.begin()), to_array<2*DIM>(padding_dims.begin()), to_array<DIM>(dilation_dims.begin()));
+        return PaddedConv<DIM>(in_channels, out_channels, to_array<DIM>(kernel_dims.begin()), name, to_array<DIM>(stride_dims.begin()), to_array<2*DIM>(padding_dims.begin()), to_array<DIM>(dilation_dims.begin()), no_bias);
     }, py::arg("in_channels"),
        py::arg("out_channels"),
        py::arg("kernel_dims"),
        py::arg("name") = "",
        py::arg("stride_dims") = std::vector<DimSize_t>(DIM,1),
        py::arg("padding_dims") = std::vector<DimSize_t>(2*DIM,0),
-       py::arg("dilation_dims") = std::vector<DimSize_t>(DIM,1));
+       py::arg("dilation_dims") = std::vector<DimSize_t>(DIM,1),
+       py::arg("no_bias")= false);
 }
 
 template <DimIdx_t DIM> void declare_PaddedConvDepthWiseOp(py::module &m) {
@@ -53,20 +55,22 @@ template <DimIdx_t DIM> void declare_PaddedConvDepthWiseOp(py::module &m) {
                                                          const std::string& name,
                                                          const std::vector<DimSize_t> &stride_dims,
                                                          const std::vector<DimSize_t> &padding_dims,
-                                                         const std::vector<DimSize_t> &dilation_dims)
+                                                         const std::vector<DimSize_t> &dilation_dims,
+                                                         bool no_bias)
     {
-        AIDGE_ASSERT(kernel_dims.size() == DIM, "kernel_dims size [%ld] does not match DIM [%d]", kernel_dims.size(), DIM);
-        AIDGE_ASSERT(stride_dims.size() == DIM, "stride_dims size [%ld] does not match DIM [%d]", stride_dims.size(), DIM);
-        AIDGE_ASSERT(padding_dims.size() == 2*DIM, "padding_dims size [%ld] does not match DIM [%d]", padding_dims.size(), 2*DIM);
-        AIDGE_ASSERT(dilation_dims.size() == DIM, "dilation_dims size [%ld] does not match DIM [%d]", dilation_dims.size(), DIM);
+        AIDGE_ASSERT(kernel_dims.size() == DIM, "kernel_dims size [{}] does not match DIM [{}]", kernel_dims.size(), DIM);
+        AIDGE_ASSERT(stride_dims.size() == DIM, "stride_dims size [{}] does not match DIM [{}]", stride_dims.size(), DIM);
+        AIDGE_ASSERT(padding_dims.size() == 2*DIM, "padding_dims size [{}] does not match DIM [{}]", padding_dims.size(), 2*DIM);
+        AIDGE_ASSERT(dilation_dims.size() == DIM, "dilation_dims size [{}] does not match DIM [{}]", dilation_dims.size(), DIM);
 
-        return PaddedConvDepthWise<DIM>(nb_channels, to_array<DIM>(kernel_dims.begin()), name, to_array<DIM>(stride_dims.begin()), to_array<2*DIM>(padding_dims.begin()), to_array<DIM>(dilation_dims.begin()));
+        return PaddedConvDepthWise<DIM>(nb_channels, to_array<DIM>(kernel_dims.begin()), name, to_array<DIM>(stride_dims.begin()), to_array<2*DIM>(padding_dims.begin()), to_array<DIM>(dilation_dims.begin()), no_bias);
     }, py::arg("nb_channels"),
        py::arg("kernel_dims"),
        py::arg("name") = "",
        py::arg("stride_dims") = std::vector<DimSize_t>(DIM,1),
        py::arg("padding_dims") = std::vector<DimSize_t>(2*DIM,0),
-       py::arg("dilation_dims") = std::vector<DimSize_t>(DIM,1));
+       py::arg("dilation_dims") = std::vector<DimSize_t>(DIM,1),
+       py::arg("no_bias") = false);
 
 }
 
@@ -76,9 +80,9 @@ template <DimIdx_t DIM> void declare_PaddedAvgPoolingOp(py::module &m) {
                                                          const std::vector<DimSize_t> &stride_dims,
                                                          const std::vector<DimSize_t> &padding_dims)
     {
-        AIDGE_ASSERT(kernel_dims.size() == DIM, "kernel_dims size [%ld] does not match DIM [%d]", kernel_dims.size(), DIM);
-        AIDGE_ASSERT(stride_dims.size() == DIM, "stride_dims size [%ld] does not match DIM [%d]", stride_dims.size(), DIM);
-        AIDGE_ASSERT(padding_dims.size() == 2*DIM, "padding_dims size [%ld] does not match DIM [%d]", padding_dims.size(), 2*DIM);
+        AIDGE_ASSERT(kernel_dims.size() == DIM, "kernel_dims size [{}] does not match DIM [{}]", kernel_dims.size(), DIM);
+        AIDGE_ASSERT(stride_dims.size() == DIM, "stride_dims size [{}] does not match DIM [{}]", stride_dims.size(), DIM);
+        AIDGE_ASSERT(padding_dims.size() == 2*DIM, "padding_dims size [{}] does not match DIM [{}]", padding_dims.size(), 2*DIM);
 
         return PaddedAvgPooling<DIM>(to_array<DIM>(kernel_dims.begin()), name, to_array<DIM>(stride_dims.begin()), to_array<2*DIM>(padding_dims.begin()));
     }, py::arg("kernel_dims"),
@@ -95,9 +99,9 @@ template <DimIdx_t DIM> void declare_PaddedMaxPoolingOp(py::module &m) {
                                                          const std::vector<DimSize_t> &padding_dims,
                                                          bool ceil_mode)
     {
-        AIDGE_ASSERT(kernel_dims.size() == DIM, "kernel_dims size [%ld] does not match DIM [%d]", kernel_dims.size(), DIM);
-        AIDGE_ASSERT(stride_dims.size() == DIM, "stride_dims size [%ld] does not match DIM [%d]", stride_dims.size(), DIM);
-        AIDGE_ASSERT(padding_dims.size() == 2*DIM, "padding_dims size [%ld] does not match DIM [%d]", padding_dims.size(), 2*DIM);
+        AIDGE_ASSERT(kernel_dims.size() == DIM, "kernel_dims size [{}] does not match DIM [{}]", kernel_dims.size(), DIM);
+        AIDGE_ASSERT(stride_dims.size() == DIM, "stride_dims size [{}] does not match DIM [{}]", stride_dims.size(), DIM);
+        AIDGE_ASSERT(padding_dims.size() == 2*DIM, "padding_dims size [{}] does not match DIM [{}]", padding_dims.size(), 2*DIM);
 
         return PaddedMaxPooling<DIM>(to_array<DIM>(kernel_dims.begin()), name, to_array<DIM>(stride_dims.begin()), to_array<2*DIM>(padding_dims.begin()), ceil_mode);
     }, py::arg("kernel_dims"),
@@ -108,6 +112,14 @@ template <DimIdx_t DIM> void declare_PaddedMaxPoolingOp(py::module &m) {
 
 }
 
+void declare_LSTMOp(py::module &m) {
+  m.def("LSTM", &LSTM, py::arg("in_channels"),
+       py::arg("hidden_channels"),
+       py::arg("seq_length"),
+       py::arg("nobias") = false,
+       py::arg("name") = "");
+}
+
 void init_MetaOperatorDefs(py::module &m) {
   declare_PaddedConvOp<1>(m);
   declare_PaddedConvOp<2>(m);
@@ -121,8 +133,12 @@ void init_MetaOperatorDefs(py::module &m) {
   declare_PaddedMaxPoolingOp<1>(m);
   declare_PaddedMaxPoolingOp<2>(m);
   declare_PaddedMaxPoolingOp<3>(m);
+  declare_LSTMOp(m);
 
   py::class_<MetaOperator_Op, std::shared_ptr<MetaOperator_Op>, OperatorTensor>(m, "MetaOperator_Op", py::multiple_inheritance())
+  .def(py::init<const char *, const std::shared_ptr<GraphView>&>(),
+          py::arg("type"),
+          py::arg("graph"))
   .def("get_micro_graph", &MetaOperator_Op::getMicroGraph);
 
   m.def("meta_operator", &MetaOperator,
diff --git a/python_binding/operator/pybind_Mul.cpp b/python_binding/operator/pybind_Mul.cpp
index 21f510d98728fbe5401288a366294241b5f10a3f..47c84c0e52f605a5466a63a5a5d0851fecedd2f8 100644
--- a/python_binding/operator/pybind_Mul.cpp
+++ b/python_binding/operator/pybind_Mul.cpp
@@ -11,6 +11,7 @@
 
 #include <pybind11/pybind11.h>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Mul.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 
@@ -21,7 +22,7 @@ void init_Mul(py::module& m) {
     py::class_<Mul_Op, std::shared_ptr<Mul_Op>, OperatorTensor>(m, "MulOp", py::multiple_inheritance())
     .def("get_inputs_name", &Mul_Op::getInputsName)
     .def("get_outputs_name", &Mul_Op::getOutputsName);
-
+    declare_registrable<Mul_Op>(m, "MulOp");
     m.def("Mul", &Mul, py::arg("name") = "");
 }
 }  // namespace Aidge
diff --git a/python_binding/operator/pybind_Operator.cpp b/python_binding/operator/pybind_Operator.cpp
index 79a85cb92cf27c7edb745c36eefe61ae86c66786..4796917fbe34dbf3b7455841c9e3f1c13ca9c64d 100644
--- a/python_binding/operator/pybind_Operator.cpp
+++ b/python_binding/operator/pybind_Operator.cpp
@@ -1,3 +1,4 @@
+
 /********************************************************************************
  * Copyright (c) 2023 CEA-List
  *
@@ -9,11 +10,16 @@
  *
  ********************************************************************************/
 
+#include <memory>
+#include <string>
+
 #include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+
 #include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Data.hpp"
 #include "aidge/operator/Operator.hpp"
 #include "aidge/utils/Types.h"
-#include <pybind11/stl.h>
 
 namespace py = pybind11;
 namespace Aidge {
@@ -32,10 +38,11 @@ void init_Operator(py::module& m){
     .def("set_datatype", &Operator::setDataType, py::arg("dataType"))
     .def("set_backend", &Operator::setBackend, py::arg("name"), py::arg("device") = 0)
     .def("forward", &Operator::forward)
-    // py::keep_alive forbide Python to garbage collect implementation will the Operator is not garbade collected !
+    // py::keep_alive forbide Python to garbage collect the implementation lambda as long as the Operator is not deleted !
     .def("set_impl", &Operator::setImpl, py::arg("implementation"), py::keep_alive<1, 2>())
+    .def("get_impl", &Operator::getImpl)
     .def("get_hook", &Operator::getHook)
     .def("add_hook", &Operator::addHook)
     ;
 }
-}
\ No newline at end of file
+}
diff --git a/python_binding/operator/pybind_OperatorTensor.cpp b/python_binding/operator/pybind_OperatorTensor.cpp
index 386a3af6c7c6e9dfad34ec2e56189a53797b59d9..c56e80a47e1142900ff844e7d9889011dee65060 100644
--- a/python_binding/operator/pybind_OperatorTensor.cpp
+++ b/python_binding/operator/pybind_OperatorTensor.cpp
@@ -9,11 +9,17 @@
  *
  ********************************************************************************/
 
+#include <memory>
+#include <string>
+
 #include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+
 #include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Data.hpp"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/operator/Operator.hpp"
-#include <pybind11/stl.h>
 
 namespace py = pybind11;
 namespace Aidge {
@@ -24,6 +30,7 @@ void init_OperatorTensor(py::module& m){
 
     .def("set_output", (void (OperatorTensor::*)(const IOIndex_t, const std::shared_ptr<Data>&)) &OperatorTensor::setOutput, py::arg("outputIdx"), py::arg("data"))
     .def("set_input", (void (OperatorTensor::*)(const IOIndex_t, const std::shared_ptr<Data>&)) &OperatorTensor::setInput, py::arg("outputIdx"), py::arg("data"))
+    .def("compute_output_dims", &OperatorTensor::computeOutputDims)
     .def("output_dims_forwarded", &OperatorTensor::outputDimsForwarded)
     ;
 }
diff --git a/python_binding/operator/pybind_Pad.cpp b/python_binding/operator/pybind_Pad.cpp
index 0956d6260e50d3be2418b1cf4089df87e442e54a..1cd9f074fe5241be11da0ea7d0d1ed5a1c5869c2 100644
--- a/python_binding/operator/pybind_Pad.cpp
+++ b/python_binding/operator/pybind_Pad.cpp
@@ -9,14 +9,14 @@
  *
  ********************************************************************************/
 
+#include <array>
 #include <pybind11/pybind11.h>
 #include <pybind11/stl.h>
-#include <iostream>
 #include <string>
 #include <vector>
-#include <array>
 
 #include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Pad.hpp"
 #include "aidge/operator/Operator.hpp"
 #include "aidge/utils/Types.h"
@@ -25,8 +25,9 @@ namespace py = pybind11;
 namespace Aidge {
 
 template <DimIdx_t DIM> void declare_PadOp(py::module &m) {
-  py::class_<Pad_Op<DIM>, std::shared_ptr<Pad_Op<DIM>>, Operator, Attributes>(
-    m, ("PadOp" + std::to_string(DIM) + "D").c_str(),
+  const std::string pyClassName("PadOp" + std::to_string(DIM) + "D");
+  py::class_<Pad_Op<DIM>, std::shared_ptr<Pad_Op<DIM>>, Attributes, Operator>(
+    m, pyClassName.c_str(),
     py::multiple_inheritance())
   .def(py::init<const std::array<DimSize_t, 2*DIM> &,
                 const PadBorderType &,
@@ -36,13 +37,14 @@ template <DimIdx_t DIM> void declare_PadOp(py::module &m) {
         py::arg("borderValue") = 0.0)
     .def("get_inputs_name", &Pad_Op<DIM>::getInputsName)
     .def("get_outputs_name", &Pad_Op<DIM>::getOutputsName)
+    .def("attributes_name", &Pad_Op<DIM>::staticGetAttrsName)
     ;
-
+  declare_registrable<Pad_Op<DIM>>(m, pyClassName);
   m.def(("Pad" + std::to_string(DIM) + "D").c_str(), [](const std::vector<DimSize_t>& beginEndTuples,
                                                         const std::string& name,
                                                         const PadBorderType &borderType = PadBorderType::Constant,
                                                         double borderValue = 0.0) {
-        AIDGE_ASSERT(beginEndTuples.size() == 2*DIM, "begin_end_tuples size [%ld] does not match DIM [%d]", beginEndTuples.size(), 2*DIM);
+        AIDGE_ASSERT(beginEndTuples.size() == 2*DIM, "begin_end_tuples size [{}] does not match DIM [{}]", beginEndTuples.size(), 2*DIM);
         return Pad<DIM>(to_array<2*DIM>(beginEndTuples.begin()), name, borderType, borderValue);
     },
        py::arg("begin_end_tuples"),
diff --git a/python_binding/operator/pybind_Pop.cpp b/python_binding/operator/pybind_Pop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..baae552270a4776d292047140e213dbe1566d35e
--- /dev/null
+++ b/python_binding/operator/pybind_Pop.cpp
@@ -0,0 +1,28 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <pybind11/pybind11.h>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/operator/Pop.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+
+namespace py = pybind11;
+namespace Aidge {
+
+void init_Pop(py::module& m) {
+    py::class_<Pop_Op, std::shared_ptr<Pop_Op>, OperatorTensor, Attributes>(m, "PopOp", py::multiple_inheritance())
+    .def("get_inputs_name", &Pop_Op::getInputsName)
+    .def("get_outputs_name", &Pop_Op::getOutputsName);
+
+    m.def("Pop", &Pop, py::arg("name") = "");
+}
+}  // namespace Aidge
diff --git a/python_binding/operator/pybind_Pow.cpp b/python_binding/operator/pybind_Pow.cpp
index 09d1e4ad2ad6413901c28bc9d9fe16995483da05..9e9ef772cadddb1c7928060b503c388b094ed9f4 100644
--- a/python_binding/operator/pybind_Pow.cpp
+++ b/python_binding/operator/pybind_Pow.cpp
@@ -11,6 +11,7 @@
 
 #include <pybind11/pybind11.h>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Pow.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 
@@ -21,6 +22,7 @@ void init_Pow(py::module& m) {
     py::class_<Pow_Op, std::shared_ptr<Pow_Op>, OperatorTensor>(m, "PowOp", py::multiple_inheritance())
     .def("get_inputs_name", &Pow_Op::getInputsName)
     .def("get_outputs_name", &Pow_Op::getOutputsName);
+    declare_registrable<Pow_Op>(m, "PowOp");
 
     m.def("Pow", &Pow, py::arg("name") = "");
 }
diff --git a/python_binding/operator/pybind_Producer.cpp b/python_binding/operator/pybind_Producer.cpp
index 78d9ce3489a8309c42cc90189e588a448fd9649a..eb74515915c252d50a2522cae6d6f4c6832ab3ef 100644
--- a/python_binding/operator/pybind_Producer.cpp
+++ b/python_binding/operator/pybind_Producer.cpp
@@ -12,11 +12,11 @@
 #include <pybind11/pybind11.h>
 #include <pybind11/stl.h>
 
-#include "aidge/utils/Types.h"
 // #include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/operator/Producer.hpp"
-#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/Types.h"
 
 namespace py = pybind11;
 namespace Aidge {
@@ -26,19 +26,21 @@ void declare_Producer(py::module &m) {
     // m.def(("Producer_" + std::to_string(DIM)+"D").c_str(), py::overload_cast<shared_ptr<Node>&>(&Producer<DIM>), py::arg("dims"), py::arg("name"));
     m.def("Producer", static_cast<std::shared_ptr<Node>(*)(const std::array<DimSize_t, DIM>&, const std::string&, bool)>(&Producer), py::arg("dims"), py::arg("name") = "", py::arg("constant") = false);
 
+
 }
 
 
 void init_Producer(py::module &m) {
-    py::class_<Producer_Op,  std::shared_ptr<Producer_Op>, OperatorTensor, Attributes>(
+    py::class_<Producer_Op,  std::shared_ptr<Producer_Op>, Attributes, OperatorTensor>(
         m,
         "ProducerOp",
         py::multiple_inheritance())
     .def("dims", &Producer_Op::dims)
     .def("get_inputs_name", &Producer_Op::getInputsName)
-    .def("get_outputs_name", &Producer_Op::getOutputsName);
+    .def("get_outputs_name", &Producer_Op::getOutputsName)
+    .def("attributes_name", &Producer_Op::staticGetAttrsName);
     m.def("Producer", static_cast<std::shared_ptr<Node>(*)(const std::shared_ptr<Tensor>, const std::string&, bool)>(&Producer), py::arg("tensor"), py::arg("name") = "", py::arg("constant") = false);
-
+    declare_registrable<Producer_Op>(m, "ProducerOp");
     declare_Producer<1>(m);
     declare_Producer<2>(m);
     declare_Producer<3>(m);
diff --git a/python_binding/operator/pybind_ReLU.cpp b/python_binding/operator/pybind_ReLU.cpp
index 24ae96649a87ff9acc996715d3cd00a97c393578..57601e25607a40c44c400fe75965d83050a146ed 100644
--- a/python_binding/operator/pybind_ReLU.cpp
+++ b/python_binding/operator/pybind_ReLU.cpp
@@ -11,6 +11,7 @@
 
 #include <pybind11/pybind11.h>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/ReLU.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 
@@ -21,6 +22,7 @@ void init_ReLU(py::module& m) {
     py::class_<ReLU_Op, std::shared_ptr<ReLU_Op>, OperatorTensor>(m, "ReLUOp", py::multiple_inheritance())
     .def("get_inputs_name", &ReLU_Op::getInputsName)
     .def("get_outputs_name", &ReLU_Op::getOutputsName);
+    declare_registrable<ReLU_Op>(m, "ReLUOp");
 
     m.def("ReLU", &ReLU, py::arg("name") = "");
 }
diff --git a/python_binding/operator/pybind_ReduceMean.cpp b/python_binding/operator/pybind_ReduceMean.cpp
index e5de98b69adde5133dde302f7306bc8a5c471eef..599a648a3f2733acd49bbbc293cd30734e8ea2ff 100644
--- a/python_binding/operator/pybind_ReduceMean.cpp
+++ b/python_binding/operator/pybind_ReduceMean.cpp
@@ -9,13 +9,14 @@
  *
  ********************************************************************************/
 
+#include <array>
 #include <pybind11/pybind11.h>
 #include <pybind11/stl.h>
 #include <string>
 #include <vector>
-#include <array>
 
 #include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/operator/ReduceMean.hpp"
 #include "aidge/utils/Types.h"
@@ -23,19 +24,22 @@
 namespace py = pybind11;
 namespace Aidge {
 
-template <DimIdx_t DIM> void declare_ReduceMeanOp(py::module &m) {
-  py::class_<ReduceMean_Op<DIM>, std::shared_ptr<ReduceMean_Op<DIM>>, OperatorTensor, Attributes>(
-    m, ("ReduceMeanOp" + std::to_string(DIM) + "D").c_str(), py::multiple_inheritance())
-    .def("get_inputs_name", &ReduceMean_Op<DIM>::getInputsName)
-    .def("get_outputs_name", &ReduceMean_Op<DIM>::getOutputsName)
+void declare_ReduceMeanOp(py::module &m) {
+  const std::string pyClassName("ReduceMeanOp");
+  py::class_<ReduceMean_Op, std::shared_ptr<ReduceMean_Op>, Attributes, OperatorTensor>(
+    m, pyClassName.c_str(), py::multiple_inheritance())
+    .def("get_inputs_name", &ReduceMean_Op::getInputsName)
+    .def("get_outputs_name", &ReduceMean_Op::getOutputsName)
+    .def("attributes_name", &ReduceMean_Op::staticGetAttrsName)
     ;
+  declare_registrable<ReduceMean_Op>(m, pyClassName);
 
-  m.def(("ReduceMean" + std::to_string(DIM) + "D").c_str(), [](const std::vector<int>& axes,
+  m.def("ReduceMean", [](const std::vector<int>& axes,
                                                                 DimSize_t keepDims,
                                                                 const std::string& name) {
-        AIDGE_ASSERT(axes.size() == DIM, "axes size [%ld] does not match DIM [%d]", axes.size(), DIM);
+        // AIDGE_ASSERT(axes.size() == DIM, "axes size [{}] does not match DIM [{}]", axes.size(), DIM);
 
-        return ReduceMean<DIM>(to_array<DIM>(axes.begin()), keepDims, name);
+        return ReduceMean(axes, keepDims, name);
     }, py::arg("axes"),
        py::arg("keep_dims") = 1,
        py::arg("name") = "");
@@ -43,9 +47,9 @@ template <DimIdx_t DIM> void declare_ReduceMeanOp(py::module &m) {
 
 
 void init_ReduceMean(py::module &m) {
-  declare_ReduceMeanOp<1>(m);
-  declare_ReduceMeanOp<2>(m);
-  declare_ReduceMeanOp<3>(m);
+  declare_ReduceMeanOp(m);
+//   declare_ReduceMeanOp<2>(m);
+//   declare_ReduceMeanOp<3>(m);
 
   // FIXME:
   // m.def("ReduceMean1D", static_cast<NodeAPI(*)(const char*, int, int, int const
diff --git a/python_binding/operator/pybind_Reshape.cpp b/python_binding/operator/pybind_Reshape.cpp
index d34a411c719bdbb1144edaa65b50050d705e0d90..0e336db28ddba4629e61d30e026befe4240c40b6 100644
--- a/python_binding/operator/pybind_Reshape.cpp
+++ b/python_binding/operator/pybind_Reshape.cpp
@@ -11,6 +11,7 @@
 
 #include <pybind11/pybind11.h>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Reshape.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 
@@ -18,10 +19,10 @@ namespace py = pybind11;
 namespace Aidge {
 
 void init_Reshape(py::module& m) {
-    py::class_<Reshape_Op, std::shared_ptr<Reshape_Op>, OperatorTensor>(m, "ReshapeOp", py::multiple_inheritance())
+    py::class_<Reshape_Op, std::shared_ptr<Reshape_Op>, Attributes, OperatorTensor>(m, "ReshapeOp", py::multiple_inheritance())
     .def("get_inputs_name", &Reshape_Op::getInputsName)
     .def("get_outputs_name", &Reshape_Op::getOutputsName);
-
+    declare_registrable<Reshape_Op>(m, "ReshapeOp");
     m.def("Reshape", &Reshape, py::arg("shape"), py::arg("name") = "");
 }
 }  // namespace Aidge
diff --git a/python_binding/operator/pybind_Sigmoid.cpp b/python_binding/operator/pybind_Sigmoid.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8ffa8581593af9dc994baa566475317bcd96d475
--- /dev/null
+++ b/python_binding/operator/pybind_Sigmoid.cpp
@@ -0,0 +1,28 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <pybind11/pybind11.h>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/operator/Sigmoid.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+
+namespace py = pybind11;
+namespace Aidge {
+
+void init_Sigmoid(py::module& m) {
+    py::class_<Sigmoid_Op, std::shared_ptr<Sigmoid_Op>, OperatorTensor>(m, "SigmoidOp", py::multiple_inheritance())
+    .def("get_inputs_name", &Sigmoid_Op::getInputsName)
+    .def("get_outputs_name", &Sigmoid_Op::getOutputsName);
+
+    m.def("Sigmoid", &Sigmoid, py::arg("name") = "");
+}
+}  // namespace Aidge
diff --git a/python_binding/operator/pybind_Slice.cpp b/python_binding/operator/pybind_Slice.cpp
index 7bfd1b4f00579ed29658db73b71f2c596048fe75..558fc98c172ea1a264ee8ac3ebbc70e09eba826d 100644
--- a/python_binding/operator/pybind_Slice.cpp
+++ b/python_binding/operator/pybind_Slice.cpp
@@ -11,6 +11,7 @@
 
 #include <pybind11/pybind11.h>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Slice.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 
@@ -21,7 +22,7 @@ void init_Slice(py::module& m) {
     py::class_<Slice_Op, std::shared_ptr<Slice_Op>, OperatorTensor>(m, "SliceOp", py::multiple_inheritance())
     .def("get_inputs_name", &Slice_Op::getInputsName)
     .def("get_outputs_name", &Slice_Op::getOutputsName);
-
+    declare_registrable<Slice_Op>(m, "SliceOp");
     m.def("Slice", &Slice, py::arg("starts"), py::arg("ends"), py::arg("axes"), py::arg("name") = "");
 }
 }  // namespace Aidge
diff --git a/python_binding/operator/pybind_Softmax.cpp b/python_binding/operator/pybind_Softmax.cpp
index 04e92d39971a731931397e943aba6e296a81a14d..837f3ed2b92aeab5739d07a04b071040806d8a1f 100644
--- a/python_binding/operator/pybind_Softmax.cpp
+++ b/python_binding/operator/pybind_Softmax.cpp
@@ -12,6 +12,7 @@
 #include <pybind11/pybind11.h>
 #include <string>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Softmax.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 
@@ -19,10 +20,11 @@ namespace py = pybind11;
 namespace Aidge {
 
 void init_Softmax(py::module& m) {
-    py::class_<Softmax_Op, std::shared_ptr<Softmax_Op>, OperatorTensor, Attributes>(m, "SoftmaxOp", py::multiple_inheritance())
+    py::class_<Softmax_Op, std::shared_ptr<Softmax_Op>, Attributes, OperatorTensor>(m, "SoftmaxOp", py::multiple_inheritance())
     .def("get_inputs_name", &Softmax_Op::getInputsName)
-    .def("get_outputs_name", &Softmax_Op::getOutputsName);
-
+    .def("get_outputs_name", &Softmax_Op::getOutputsName)
+    .def("attributes_name", &Softmax_Op::staticGetAttrsName);
+    declare_registrable<Softmax_Op>(m, "SoftmaxOp");
     m.def("Softmax", &Softmax, py::arg("axis"), py::arg("name") = "");
 }
 }  // namespace Aidge
diff --git a/python_binding/operator/pybind_Sqrt.cpp b/python_binding/operator/pybind_Sqrt.cpp
index 98d65242e8ff199992bbfc740192ae25e6d7b738..7065b828eb18d77edce49726dd903045c7952977 100644
--- a/python_binding/operator/pybind_Sqrt.cpp
+++ b/python_binding/operator/pybind_Sqrt.cpp
@@ -11,6 +11,7 @@
 
 #include <pybind11/pybind11.h>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Sqrt.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 
@@ -21,7 +22,7 @@ void init_Sqrt(py::module& m) {
     py::class_<Sqrt_Op, std::shared_ptr<Sqrt_Op>, OperatorTensor>(m, "SqrtOp", py::multiple_inheritance())
     .def("get_inputs_name", &Sqrt_Op::getInputsName)
     .def("get_outputs_name", &Sqrt_Op::getOutputsName);
-
+    declare_registrable<Sqrt_Op>(m, "SqrtOp");
     m.def("Sqrt", &Sqrt, py::arg("name") = "");
 }
 }  // namespace Aidge
diff --git a/python_binding/operator/pybind_Sub.cpp b/python_binding/operator/pybind_Sub.cpp
index dce1ab6cb27cc7da02e6c817a6bc49ec64bcf364..e031040dfe8373c07d1524cbe4f75f3744e2f312 100644
--- a/python_binding/operator/pybind_Sub.cpp
+++ b/python_binding/operator/pybind_Sub.cpp
@@ -11,6 +11,7 @@
 
 #include <pybind11/pybind11.h>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Sub.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 
@@ -21,7 +22,7 @@ void init_Sub(py::module& m) {
     py::class_<Sub_Op, std::shared_ptr<Sub_Op>, OperatorTensor>(m, "SubOp", py::multiple_inheritance())
     .def("get_inputs_name", &Sub_Op::getInputsName)
     .def("get_outputs_name", &Sub_Op::getOutputsName);
-
+    declare_registrable<Sub_Op>(m, "SubOp");
     m.def("Sub", &Sub, py::arg("name") = "");
 }
 }  // namespace Aidge
diff --git a/python_binding/operator/pybind_Tanh.cpp b/python_binding/operator/pybind_Tanh.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a5c2f9dd5f2eab17e296f82788726210f976bd0d
--- /dev/null
+++ b/python_binding/operator/pybind_Tanh.cpp
@@ -0,0 +1,28 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <pybind11/pybind11.h>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/operator/Tanh.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+
+namespace py = pybind11;
+namespace Aidge {
+
+void init_Tanh(py::module& m) {
+    py::class_<Tanh_Op, std::shared_ptr<Tanh_Op>, OperatorTensor>(m, "TanhOp", py::multiple_inheritance())
+    .def("get_inputs_name", &Tanh_Op::getInputsName)
+    .def("get_outputs_name", &Tanh_Op::getOutputsName);
+
+    m.def("Tanh", &Tanh, py::arg("name") = "");
+}
+}  // namespace Aidge
diff --git a/python_binding/operator/pybind_Transpose.cpp b/python_binding/operator/pybind_Transpose.cpp
index e92e9c2aaafe2d20220da053a2b9d799fbe8466d..f6e2f2225e4858d3385c5d0140a863e7e7705652 100644
--- a/python_binding/operator/pybind_Transpose.cpp
+++ b/python_binding/operator/pybind_Transpose.cpp
@@ -17,24 +17,28 @@
 #include <array>
 
 #include "aidge/backend/OperatorImpl.hpp"
-#include "aidge/operator/Transpose.hpp"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/operator/Transpose.hpp"
 #include "aidge/utils/Types.h"
-#include "aidge/data/Tensor.hpp"
 
 namespace py = pybind11;
 namespace Aidge {
 
-template <DimIdx_t DIM> 
+template <DimIdx_t DIM>
 void declare_Transpose(py::module &m) {
-  py::class_<Transpose_Op<DIM>, std::shared_ptr<Transpose_Op<DIM>>, OperatorTensor, Attributes>(
+  const std::string pyClassName("TransposeOp" + std::to_string(DIM) + "D");
+  py::class_<Transpose_Op<DIM>, std::shared_ptr<Transpose_Op<DIM>>, Attributes, OperatorTensor>(
     m, ("TransposeOp" + std::to_string(DIM) + "D").c_str(), py::multiple_inheritance())
   .def("get_inputs_name", &Transpose_Op<DIM>::getInputsName)
-  .def("get_outputs_name", &Transpose_Op<DIM>::getOutputsName);
+  .def("get_outputs_name", &Transpose_Op<DIM>::getOutputsName)
+  .def("attributes_name", &Transpose_Op<DIM>::staticGetAttrsName);
+
+  declare_registrable<Transpose_Op<DIM>>(m, pyClassName);
 
   m.def(("Transpose" + std::to_string(DIM) + "D").c_str(), [](const std::vector<DimSize_t>& output_dims_order,
                                                                   const std::string& name) {
-        AIDGE_ASSERT(output_dims_order.size() == DIM, "output_dims_order size [%ld] does not match DIM [%d]", output_dims_order.size(), DIM);
+        AIDGE_ASSERT(output_dims_order.size() == DIM, "output_dims_order size [{}] does not match DIM [{}]", output_dims_order.size(), DIM);
         return Transpose<DIM>(to_array<DIM>(output_dims_order.begin()), name);
     }, py::arg("output_dims_order"),
        py::arg("name") = "");
diff --git a/python_binding/pybind_core.cpp b/python_binding/pybind_core.cpp
index be0d357b7f73e26aad44994f407696f70617ad71..63e5100ac65b5582c7236c2b3467a7d1debcaa36 100644
--- a/python_binding/pybind_core.cpp
+++ b/python_binding/pybind_core.cpp
@@ -11,13 +11,19 @@
 
 #include <pybind11/pybind11.h>
 
+#include "aidge/backend/cpu/data/TensorImpl.hpp"  // This include add Tensor
+
 namespace py = pybind11;
 
 namespace Aidge {
+void init_Random(py::module&);
 void init_Data(py::module&);
+void init_Database(py::module&);
+void init_DataProvider(py::module&);
 void init_Tensor(py::module&);
 void init_OperatorImpl(py::module&);
 void init_Attributes(py::module&);
+void init_Log(py::module&);
 void init_Operator(py::module&);
 void init_OperatorTensor(py::module&);
 
@@ -32,6 +38,7 @@ void init_Erf(py::module&);
 void init_FC(py::module&);
 void init_Gather(py::module&);
 void init_GenericOperator(py::module&);
+void init_GlobalAveragePooling(py::module&);
 void init_LeakyReLU(py::module&);
 void init_MatMul(py::module&);
 void init_MaxPooling(py::module&);
@@ -39,14 +46,17 @@ void init_MetaOperatorDefs(py::module&);
 void init_Mul(py::module&);
 void init_Producer(py::module&);
 void init_Pad(py::module&);
+void init_Pop(py::module&);
 void init_Pow(py::module&);
 void init_ReduceMean(py::module&);
 void init_ReLU(py::module&);
 void init_Reshape(py::module&);
+void init_Sigmoid(py::module&);
 void init_Slice(py::module&);
 void init_Softmax(py::module&);
 void init_Sqrt(py::module&);
 void init_Sub(py::module&);
+void init_Tanh(py::module&);
 void init_Transpose(py::module&);
 void init_Identity(py::module&);
 
@@ -58,14 +68,19 @@ void init_Connector(py::module&);
 void init_GraphRegex(py::module&);
 void init_MatchSolution(py::module&);
 
-void init_Recipies(py::module&);
+void init_Recipes(py::module&);
+void init_GraphViewHelper(py::module&);
 
 void init_Scheduler(py::module&);
 void init_TensorUtils(py::module&);
+void init_Filler(py::module&);
 
+void init_Aidge(py::module& m) {
+    init_Random(m);
 
-void init_Aidge(py::module& m){
     init_Data(m);
+    init_Database(m);
+    init_DataProvider(m);
     init_Tensor(m);
 
     init_Node(m);
@@ -75,6 +90,7 @@ void init_Aidge(py::module& m){
 
     init_OperatorImpl(m);
     init_Attributes(m);
+    init_Log(m);
     init_Operator(m);
     init_OperatorTensor(m);
     init_Add(m);
@@ -88,6 +104,7 @@ void init_Aidge(py::module& m){
     init_FC(m);
     init_Gather(m);
     init_GenericOperator(m);
+    init_GlobalAveragePooling(m);
     init_LeakyReLU(m);
     init_MatMul(m);
     init_MaxPooling(m);
@@ -95,14 +112,17 @@ void init_Aidge(py::module& m){
     init_Mul(m);
     init_Pad(m);
 
+    init_Pop(m);
     init_Pow(m);
     init_ReduceMean(m);
     init_ReLU(m);
     init_Reshape(m);
+    init_Sigmoid(m);
     init_Slice(m);
     init_Softmax(m);
     init_Sqrt(m);
     init_Sub(m);
+    init_Tanh(m);
     init_Transpose(m);
     init_Identity(m);
 
@@ -111,12 +131,12 @@ void init_Aidge(py::module& m){
     init_GraphRegex(m);
     init_MatchSolution(m);
 
-    init_Recipies(m);
+    init_Recipes(m);
+    init_GraphViewHelper(m);
     init_Scheduler(m);
     init_TensorUtils(m);
+    init_Filler(m);
 }
 
-PYBIND11_MODULE(aidge_core, m) {
-    init_Aidge(m);
-}
-}
+PYBIND11_MODULE(aidge_core, m) { init_Aidge(m); }
+}  // namespace Aidge
diff --git a/python_binding/recipes/pybind_GraphViewHelper.cpp b/python_binding/recipes/pybind_GraphViewHelper.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ac56fb4b43eb5b0a737157ec9e64c6771a692816
--- /dev/null
+++ b/python_binding/recipes/pybind_GraphViewHelper.cpp
@@ -0,0 +1,28 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+
+#include <memory>
+#include <set>
+
+#include "aidge/graph/GraphView.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/recipes/GraphViewHelper.hpp"
+
+namespace py = pybind11;
+
+namespace Aidge {
+void init_GraphViewHelper(py::module &m) {
+    m.def("producers", &producers, py::arg("graphview"));
+}
+} // namespace Aidge
diff --git a/python_binding/recipies/pybind_Recipies.cpp b/python_binding/recipes/pybind_Recipes.cpp
similarity index 97%
rename from python_binding/recipies/pybind_Recipies.cpp
rename to python_binding/recipes/pybind_Recipes.cpp
index bd058defb21c13cea1323e4748129c92519de039..f122c411618ce28a641fd46ee568f99cc48e9f58 100644
--- a/python_binding/recipies/pybind_Recipies.cpp
+++ b/python_binding/recipes/pybind_Recipes.cpp
@@ -15,13 +15,13 @@
 #include <cstddef>
 #include <string>
 
-#include "aidge/recipies/Recipies.hpp"
+#include "aidge/recipes/Recipes.hpp"
 #include "aidge/utils/Types.h"
 
 namespace py = pybind11;
 
 namespace Aidge {
-void init_Recipies(py::module &m) {
+void init_Recipes(py::module &m) {
 
 
   m.def("fuse_mul_add", static_cast<void(*)(std::shared_ptr<GraphView>)>(fuseMulAdd), py::arg("graph_view"), R"mydelimiter(
diff --git a/python_binding/scheduler/pybind_Scheduler.cpp b/python_binding/scheduler/pybind_Scheduler.cpp
index d963b81d501f5cd2faf4f69810c897bb4b4da86d..c0966e54d4f025a607aa9763a3657de5b39d2ff4 100644
--- a/python_binding/scheduler/pybind_Scheduler.cpp
+++ b/python_binding/scheduler/pybind_Scheduler.cpp
@@ -12,18 +12,31 @@
 #include <pybind11/pybind11.h>
 #include <pybind11/stl.h>
 #include "aidge/scheduler/Scheduler.hpp"
+#include "aidge/scheduler/SequentialScheduler.hpp"
+#include "aidge/scheduler/ParallelScheduler.hpp"
 #include "aidge/graph/GraphView.hpp"
+#include "aidge/data/Tensor.hpp"
 
 namespace py = pybind11;
 namespace Aidge {
 void init_Scheduler(py::module& m){
-    py::class_<SequentialScheduler, std::shared_ptr<SequentialScheduler>>(m, "SequentialScheduler")
+    py::class_<Scheduler, std::shared_ptr<Scheduler>>(m, "Scheduler")
     .def(py::init<std::shared_ptr<GraphView>&>(), py::arg("graph_view"))
-    .def("forward", &SequentialScheduler::forward, py::arg("forward_dims")=true, py::arg("verbose")=false)
-    .def("save_scheduling_diagram", &SequentialScheduler::saveSchedulingDiagram, py::arg("file_name"))
-    .def("resetScheduling", &SequentialScheduler::resetScheduling)
-    .def("generate_scheduling", &SequentialScheduler::generateScheduling, py::arg("verbose")=false)
-    .def("get_static_scheduling", &SequentialScheduler::getStaticScheduling)
+    .def("save_scheduling_diagram", &Scheduler::saveSchedulingDiagram, py::arg("file_name"))
+    .def("resetScheduling", &Scheduler::resetScheduling)
+    .def("generate_scheduling", &Scheduler::generateScheduling)
+    .def("get_static_scheduling", &Scheduler::getStaticScheduling, py::arg("step") = 0)
+    ;
+
+    py::class_<SequentialScheduler, std::shared_ptr<SequentialScheduler>, Scheduler>(m, "SequentialScheduler")
+    .def(py::init<std::shared_ptr<GraphView>&>(), py::arg("graph_view"))
+    .def("forward", &SequentialScheduler::forward, py::arg("forward_dims")=true, py::arg("data")=std::vector<Tensor>())
+    .def("backward", &SequentialScheduler::backward, py::arg("data"), py::arg("instanciate_grad")=true)
+    ;
+
+    py::class_<ParallelScheduler, std::shared_ptr<ParallelScheduler>, Scheduler>(m, "ParallelScheduler")
+    .def(py::init<std::shared_ptr<GraphView>&>(), py::arg("graph_view"))
+    .def("forward", &ParallelScheduler::forward, py::arg("forward_dims")=true, py::arg("data")=std::vector<Tensor>())
     ;
 }
 }
diff --git a/python_binding/utils/pybind_Parameter.cpp b/python_binding/utils/pybind_Attributes.cpp
similarity index 79%
rename from python_binding/utils/pybind_Parameter.cpp
rename to python_binding/utils/pybind_Attributes.cpp
index 2957876f31ad0781a36905cef3a5ae88934b6a8a..bfce891176822a3b1c07b1ded0c46c9c94a43c0a 100644
--- a/python_binding/utils/pybind_Parameter.cpp
+++ b/python_binding/utils/pybind_Attributes.cpp
@@ -1,6 +1,7 @@
 #include <pybind11/pybind11.h>
 #include "aidge/utils/Attributes.hpp"
 #include "aidge/utils/DynamicAttributes.hpp"
+#include "aidge/utils/StaticAttributes.hpp"
 
 namespace py = pybind11;
 namespace Aidge {
@@ -21,11 +22,13 @@ void init_Attributes(py::module& m){
     .def("has_attr", &Attributes::hasAttr, py::arg("name"))
     .def("get_attr_type", &Attributes::getAttrType, py::arg("name"))
     .def("get_attrs_name", &Attributes::getAttrsName)
-    .def("get_attr", &Attributes::getAttrPy, py::arg("name"));
+    .def("get_attr", &Attributes::getAttrPy, py::arg("name"))
+    .def("__getattr__", &Attributes::getAttrPy, py::arg("name"))
+    .def("set_attr", &Attributes::setAttrPy, py::arg("name"), py::arg("value"))
+    .def("__setattr__", &Attributes::setAttrPy, py::arg("name"), py::arg("value"));
 
     py::class_<DynamicAttributes, std::shared_ptr<DynamicAttributes>, Attributes>(m, "DynamicAttributes")
     .def("add_attr", &DynamicAttributes::addAttrPy, py::arg("name"), py::arg("value"))
-    .def("set_attr", &DynamicAttributes::setAttrPy, py::arg("name"), py::arg("value"))
     .def("del_attr", &DynamicAttributes::delAttr, py::arg("name"));
 
     m.def("test_DynamicAttributes_binding", &test_DynamicAttributes_binding);
diff --git a/python_binding/utils/pybind_Log.cpp b/python_binding/utils/pybind_Log.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7b5e7548b3126ed2ebfe3d9243248dc070c54076
--- /dev/null
+++ b/python_binding/utils/pybind_Log.cpp
@@ -0,0 +1,103 @@
+#include <pybind11/pybind11.h>
+#include "aidge/utils/Log.hpp"
+
+namespace py = pybind11;
+namespace Aidge {
+void init_Log(py::module& m){
+    py::enum_<Log::Level>(m, "Level")
+        .value("Debug", Log::Debug)
+        .value("Info", Log::Info)
+        .value("Notice", Log::Notice)
+        .value("Warn", Log::Warn)
+        .value("Error", Log::Error)
+        .value("Fatal", Log::Fatal);
+
+    py::class_<Log>(m, "Log")
+    .def_static("debug", [](const std::string& msg) { Log::debug(msg); }, py::arg("msg"),
+          R"mydelimiter(
+          Detailed messages for debugging purposes, providing information helpful
+          for developers to trace and identify issues.
+          Detailed insights of what is appening in an operation, not useful for the
+          end-user. The operation is performed nominally.
+          Note: This level is disabled at compile time for Release, therefore
+          inducing no runtime overhead for Release.
+
+          :param msg: Debug message.
+          :type msg: str
+          )mydelimiter")
+    .def_static("info", [](const std::string& msg) { Log::info(msg); }, py::arg("msg"),
+          R"mydelimiter(
+          Messages that provide a record of the normal operation, about
+          the application's state, progress, or important events.
+          Reports normal start, end and key steps in an operation. The operation is
+          performed nominally.
+
+          :param msg: Info message.
+          :type msg: str
+          )mydelimiter")
+    .def_static("notice", [](const std::string& msg) { Log::notice(msg); }, py::arg("msg"),
+          R"mydelimiter(
+          Applies to normal but significant conditions that may require monitoring,
+          like unusual or normal fallback events.
+          Reports specific paths in an operation. The operation can still be
+          performed normally.
+
+          :param msg: Notice message.
+          :type msg: str
+          )mydelimiter")
+    .def_static("warn", [](const std::string& msg) { Log::warn(msg); }, py::arg("msg"),
+          R"mydelimiter(
+          Indicates potential issues or situations that may lead to errors but do
+          not necessarily cause immediate problems.
+          Some specific steps of the operation could not be performed, but it can
+          still provide an exploitable result.
+
+          :param msg: Warning message.
+          :type msg: str
+          )mydelimiter")
+    .def_static("error",[](const std::string& msg) { Log::error(msg); }, py::arg("msg"),
+          R"mydelimiter(
+          Signifies a problem or unexpected condition that the application can
+          recover from, but attention is needed to prevent further issues.
+          The operation could not be performed, but it does not prevent potential
+          further operations.
+
+          :param msg: Error message.
+          :type msg: str
+          )mydelimiter")
+    .def_static("fatal", [](const std::string& msg) { Log::fatal(msg); }, py::arg("msg"),
+          R"mydelimiter(
+          Represents a critical error or condition that leads to the termination of
+          the application, indicating a severe and unrecoverable problem.
+          The operation could not be performed and any further operation is
+          impossible.
+
+          :param msg: Fatal message.
+          :type msg: str
+          )mydelimiter")
+    .def_static("set_console_level", &Log::setConsoleLevel, py::arg("level"),
+          R"mydelimiter(
+          Set the minimum log level displayed in the console.
+
+          :param level: Log level.
+          :type level: Level
+          )mydelimiter")
+    .def_static("set_file_level", &Log::setFileLevel, py::arg("level"),
+          R"mydelimiter(
+          Set the minimum log level saved in the log file.
+
+          :param level: Log level.
+          :type level: Level
+          )mydelimiter")
+    .def_static("set_file_name", &Log::setFileName, py::arg("fileName"),
+          R"mydelimiter(
+          Set the log file name.
+          Close the current log file and open the one with the new file name.
+          If empty, stop logging into a file.
+
+          :param fileName: Log file name.
+          :type fileName: str
+          )mydelimiter");
+}
+
+}
diff --git a/python_binding/utils/pybind_Random.cpp b/python_binding/utils/pybind_Random.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a1956d2d1e398cdb81673e7760a92bcde46e2de6
--- /dev/null
+++ b/python_binding/utils/pybind_Random.cpp
@@ -0,0 +1,24 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <pybind11/pybind11.h>
+#include "aidge/utils/Random.hpp"
+
+namespace py = pybind11;
+
+namespace Aidge {
+
+void init_Random(py::module &m) {
+    auto mRand = m.def_submodule("random", "Random module.");
+    py::class_<Random::Generator>(mRand, "Generator")
+    .def_static("set_seed", Random::Generator::setSeed);
+}
+}  // namespace Aidge
diff --git a/src/backend/OperatorImpl.cpp b/src/backend/OperatorImpl.cpp
index b76bf33367221add6273e02590d6ec315cfa4544..2277db2421c36704270b81bdb6c45f19aaa891e4 100644
--- a/src/backend/OperatorImpl.cpp
+++ b/src/backend/OperatorImpl.cpp
@@ -10,49 +10,104 @@
  ********************************************************************************/
 
 #include <cassert>
+#include <string>
 
 #include "aidge/backend/OperatorImpl.hpp"
 #include "aidge/operator/Operator.hpp"
 #include "aidge/data/Tensor.hpp"
 #include "aidge/utils/ErrorHandling.hpp"
 
-Aidge::OperatorImpl::OperatorImpl(const Operator& op):
+Aidge::OperatorImpl::OperatorImpl(const Operator& op, const std::string& backend):
     mOp(op),
-    mNbConsumedData(mOp.nbInputs(), 0),
-    mNbProducedData(mOp.nbOutputs(), 0)
+    mBackend(backend),
+    mNbConsumedData(mOp.nbInputs(), Elts_t::NoneElts()),
+    mNbProducedData(mOp.nbOutputs(), Elts_t::NoneElts())
 {
     //ctor
 }
 
-Aidge::NbElts_t Aidge::OperatorImpl::getNbRequiredData(const Aidge::IOIndex_t inputIdx) const {
-    assert(mOp.getRawInput(inputIdx) && "requires valid input");
+Aidge::Elts_t Aidge::OperatorImpl::getNbRequiredData(const Aidge::IOIndex_t inputIdx) const {
+    AIDGE_ASSERT(mOp.getRawInput(inputIdx),
+        "a valid input is required at index {} for operator type {}",
+        inputIdx, mOp.type());
 
-    // Requires the whole tensor by default
-    return std::static_pointer_cast<Tensor>(mOp.getRawInput(inputIdx))->size();
+    if (mOp.getRawInput(inputIdx)) {
+        const auto input = std::static_pointer_cast<Tensor>(mOp.getRawInput(inputIdx));
+        if (!input->empty()) {
+            // Known amount of data: requires the whole tensor by default
+            return Elts_t::DataElts(input->size());
+        }
+        else {
+            // Unknown amount of data: require a single token by default
+            return Elts_t::TokenElts(1);
+        }
+    }
+
+    // Input not connected, meaning it is an optional input: do no require anything!
+    return Elts_t::NoneElts();
 }
 
-Aidge::NbElts_t Aidge::OperatorImpl::getNbRequiredProtected(IOIndex_t inputIdx) const {
-    assert(mOp.getRawInput(inputIdx) && "requires valid input");
+Aidge::Elts_t Aidge::OperatorImpl::getNbRequiredProtected(IOIndex_t inputIdx) const {
+    AIDGE_ASSERT(mOp.getRawInput(inputIdx),
+        "a valid input is required at index {} for operator type {}",
+        inputIdx, mOp.type());
+
+    if (mOp.getRawInput(inputIdx)) {
+        const auto input = std::static_pointer_cast<Tensor>(mOp.getRawInput(inputIdx));
+        if (!input->empty()) {
+            // Known amount of data: protect the whole tensor by default
+            return Elts_t::DataElts(input->size());
+        }
+        else {
+            // Unknown amount of data: protect a single token by default
+            // (this does not really make sense for now, as getNbRequiredProtected()
+            // is supposed to give a precise amount of data to protect for
+            // memory management purpose...)
+            return Elts_t::TokenElts(1);
+        }
+    }
 
-    // Protect the whole tensor by default
-    return std::static_pointer_cast<Tensor>(mOp.getRawInput(inputIdx))->size();
+    // Input not connected, meaning it is an optional input: do no require anything!
+    return Elts_t::NoneElts();
 }
 
-Aidge::NbElts_t Aidge::OperatorImpl::getRequiredMemory(const Aidge::IOIndex_t outputIdx,
+Aidge::Elts_t Aidge::OperatorImpl::getRequiredMemory(const Aidge::IOIndex_t outputIdx,
                                                          const std::vector<Aidge::DimSize_t> &/*inputsSize*/) const {
-    assert(mOp.getRawOutput(outputIdx) && "requires valid output");
+    AIDGE_ASSERT(mOp.getRawOutput(outputIdx),
+        "a valid output is required at index {} for operator type {}",
+        outputIdx, mOp.type());
 
-    // Requires the whole tensor by default, regardless of available data on inputs
-    return std::static_pointer_cast<Tensor>(mOp.getRawOutput(outputIdx))->size();
+    if (mOp.getRawOutput(outputIdx)) {
+        const auto output = std::static_pointer_cast<Tensor>(mOp.getRawOutput(outputIdx));
+        if (!output->empty()) {
+            // Known amount of data: requires the whole tensor by default,
+            // regardless of available data on inputs
+            return Elts_t::DataElts(output->size());
+        }
+        else {
+            // Unknown amount of data: require a single token by default
+            // (this does not really make sense for now, as getRequiredMemory()
+            // is supposed to give a precise amount of data to allocate for
+            // memory management purpose...)
+            return Elts_t::TokenElts(1);
+        }
+    }
+
+    // Output not set, meaning it is an optional output: do no require anything!
+    return Elts_t::NoneElts();
 }
 
-Aidge::NbElts_t Aidge::OperatorImpl::getNbConsumedData(Aidge::IOIndex_t inputIdx) const {
-    assert(static_cast<std::size_t>(inputIdx) < mNbConsumedData.size());
+Aidge::Elts_t Aidge::OperatorImpl::getNbConsumedData(Aidge::IOIndex_t inputIdx) const {
+    AIDGE_ASSERT(static_cast<std::size_t>(inputIdx) < mNbConsumedData.size(),
+        "input index ({}) is out of bound ({}) for operator type {}",
+        inputIdx, mNbConsumedData.size(), mOp.type());
     return mNbConsumedData[static_cast<std::size_t>(inputIdx)];
 }
 
-Aidge::NbElts_t Aidge::OperatorImpl::getNbProducedData(Aidge::IOIndex_t outputIdx) const {
-    assert(static_cast<std::size_t>(outputIdx) < mNbProducedData.size());
+Aidge::Elts_t Aidge::OperatorImpl::getNbProducedData(Aidge::IOIndex_t outputIdx) const {
+    AIDGE_ASSERT(static_cast<std::size_t>(outputIdx) < mNbProducedData.size(),
+        "output index ({}) is out of bound ({}) for operator type {}",
+        outputIdx, mNbProducedData.size(), mOp.type());
     return mNbProducedData[static_cast<std::size_t>(outputIdx)];
 }
 
@@ -68,6 +123,11 @@ void Aidge::OperatorImpl::updateConsummerProducer(){
     }
 }
 
+void Aidge::OperatorImpl::resetConsummerProducer(){
+    std::fill(mNbConsumedData.begin(), mNbConsumedData.end(), Elts_t::NoneElts());
+    std::fill(mNbProducedData.begin(), mNbProducedData.end(), Elts_t::NoneElts());
+}
+
 void Aidge::OperatorImpl::forward() {
     AIDGE_THROW_OR_ABORT(std::runtime_error, "forward() not implemented");
 }
diff --git a/src/backend/TensorImpl.cpp b/src/backend/TensorImpl.cpp
index 3982ee1fed9c9198b539bf9a28edd461992b791f..ee2f82a9cf847bfc6fe51e8d8b621e53a4c93cf4 100644
--- a/src/backend/TensorImpl.cpp
+++ b/src/backend/TensorImpl.cpp
@@ -14,23 +14,23 @@
 #include "aidge/utils/Types.h"
 #include "aidge/utils/ErrorHandling.hpp"
 
-void Aidge::TensorImpl::copyFrom(const TensorImpl& srcImpl, NbElts_t length) {
-    if (&srcImpl == this) {
+void Aidge::TensorImpl::copyFrom(const TensorImpl& srcImpl, NbElts_t length, NbElts_t srcOffset, NbElts_t dstOffset) {
+    if (&srcImpl == this && srcOffset == dstOffset) {
         return;
     }
 
     if (srcImpl.device() != device()) {
         if (srcImpl.backend() == backend()) {
             // Same backend, but different device
-            copyFromDevice(srcImpl.rawPtr(), length, srcImpl.device());
+            copyFromDevice(srcImpl.rawPtr(srcOffset), srcImpl.device(), length, dstOffset);
         }
         else if (srcImpl.hostPtr() != nullptr) {
             // Different backend, but input is valid on host
-            copyFromHost(srcImpl.hostPtr(), length);
+            copyFromHost(srcImpl.hostPtr(srcOffset), length, dstOffset);
         }
         else if (hostPtr() != nullptr) {
             // Different backend, but dst is valid on host
-            srcImpl.copyToHost(hostPtr(), length);
+            srcImpl.copyToHost(hostPtr(srcOffset), length, dstOffset);
         }
         else {
             // No direct link possible from src to dst device
@@ -40,12 +40,12 @@ void Aidge::TensorImpl::copyFrom(const TensorImpl& srcImpl, NbElts_t length) {
             // - There is currently no concrete use case
             // - Just providing a pointer would be unsafe (risk of buffer overflow...)
             auto tmpHostBuffer = std::unique_ptr<char[]>(new char[scalarSize() * length]);
-            srcImpl.copyToHost(tmpHostBuffer.get(), length);
-            copyFromHost(tmpHostBuffer.get(), length);
+            srcImpl.copyToHost(tmpHostBuffer.get(), length, srcOffset);
+            copyFromHost(tmpHostBuffer.get(), length, dstOffset);
         }
     }
     else {
         // Same device: simple copy on device
-        copy(srcImpl.rawPtr(), length);
+        copy(srcImpl.rawPtr(srcOffset), length, dstOffset);
     }
 }
diff --git a/src/backend/cpu/data/TensorImpl.cpp b/src/backend/cpu/data/TensorImpl.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..da90197e912fbeabc2f28bd3bedd91cc6f29e466
--- /dev/null
+++ b/src/backend/cpu/data/TensorImpl.cpp
@@ -0,0 +1,107 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include "aidge/backend/cpu/data/TensorImpl.hpp"
+
+#include <algorithm>  // std::copy
+#include <cstddef>    // std::size_t
+#include <cstdint>    // std::uint8_t, std::int8_t, std::uint16_t, std::int16_t,
+                      // std::uint32_t, std::int32_t, std::uint64_t, std::int64_t
+#include <string>
+
+#include "aidge/data/half.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Types.h"
+
+
+template <typename T>
+bool Aidge::TensorImpl_cpu<T>::operator==(const Aidge::TensorImpl &other) const {
+    const auto& typedOtherImpl = reinterpret_cast<const TensorImpl_cpu<T>&>(other);
+    AIDGE_INTERNAL_ASSERT(typedOtherImpl.size() >= mNbElts);
+
+    std::size_t i = 0;
+    for (;
+        i < mNbElts &&
+            *static_cast<const T*>(rawPtr(i)) == *static_cast<const T*>(typedOtherImpl.rawPtr(i));
+        ++i)
+    {}
+    return i == mNbElts;
+}
+
+template <typename T>
+void Aidge::TensorImpl_cpu<T>::zeros() {
+    if (mData.empty()) {
+        lazyInit();
+    }
+    for (std::size_t i = 0; i < mData.size(); ++i) {
+        *(mData.data() + i) = T(0);
+    }
+}
+
+template <typename T>
+void Aidge::TensorImpl_cpu<T>::copyCast(const void *src, const Aidge::DataType srcDt, Aidge::NbElts_t length, Aidge::NbElts_t offset) {
+    if (length == 0) {
+        return;
+    }
+
+    T* dstT = static_cast<T *>(rawPtr(offset));
+    AIDGE_ASSERT(length <= mData.size() || length <= mNbElts, "copy length is above capacity");
+    switch (srcDt)
+    {
+        case DataType::Float64:
+            std::copy(static_cast<const double*>(src), static_cast<const double*>(src) + length,
+                    dstT);
+            break;
+        case DataType::Float32:
+            std::copy(static_cast<const float*>(src), static_cast<const float*>(src) + length,
+                    dstT);
+            break;
+        case DataType::Float16:
+            std::copy(static_cast<const half_float::half*>(src), static_cast<const half_float::half*>(src) + length,
+                    dstT);
+            break;
+        case DataType::Int64:
+            std::copy(static_cast<const int64_t*>(src), static_cast<const int64_t*>(src) + length,
+                    dstT);
+            break;
+        case DataType::UInt64:
+            std::copy(static_cast<const uint64_t*>(src), static_cast<const uint64_t*>(src) + length,
+                    dstT);
+            break;
+        case DataType::Int32:
+            std::copy(static_cast<const int32_t*>(src), static_cast<const int32_t*>(src) + length,
+                    dstT);
+            break;
+        case DataType::UInt32:
+            std::copy(static_cast<const uint32_t*>(src), static_cast<const uint32_t*>(src) + length,
+                    dstT);
+            break;
+        case DataType::Int16:
+            std::copy(static_cast<const int16_t*>(src), static_cast<const int16_t*>(src) + length,
+                    dstT);
+            break;
+        case DataType::UInt16:
+            std::copy(static_cast<const uint16_t*>(src), static_cast<const uint16_t*>(src) + length,
+                    dstT);
+            break;
+        case DataType::Int8:
+            std::copy(static_cast<const int8_t*>(src), static_cast<const int8_t*>(src) + length,
+                    dstT);
+            break;
+        case DataType::UInt8:
+            std::copy(static_cast<const uint8_t*>(src), static_cast<const uint8_t*>(src) + length,
+                    dstT);
+            break;
+        default:
+            AIDGE_THROW_OR_ABORT(std::runtime_error, "Unsupported data type.");
+            break;
+    }
+}
\ No newline at end of file
diff --git a/src/data/DataProvider.cpp b/src/data/DataProvider.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5c3d1d7ef3b3dd8c779cf9cda737f1a2b2f6e01f
--- /dev/null
+++ b/src/data/DataProvider.cpp
@@ -0,0 +1,124 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <cassert>
+#include <cstddef>  // std::size_t
+#include <memory>
+#include <vector>
+#include <cmath>
+
+
+#include "aidge/data/Database.hpp"
+#include "aidge/data/DataProvider.hpp"
+#include "aidge/data/Tensor.hpp"
+
+#include "aidge/utils/Random.hpp"
+
+
+Aidge::DataProvider::DataProvider(const Aidge::Database& database, const std::size_t batchSize, const bool shuffle, const bool dropLast)
+    : mDatabase(database),
+      mBatchSize(batchSize),
+      mShuffle(shuffle),
+      mDropLast(dropLast),
+      mNumberModality(database.getItem(0).size()),
+      mNbItems(mDatabase.getLen()),
+      mIndexBatch(0)
+{
+    // Iterating on each data modality in the database
+    // Get the tensor dimensions, datatype and backend of each modality to ensure each data have the same
+    for (const auto& modality : mDatabase.getItem(0)) {
+        mDataDims.push_back(modality->dims());
+        // assert(std::strcmp(item[i]->getImpl()->backend(), "cpu") == 0 && "DataProvider currently only supports cpu backend tensors");
+        mDataTypes.push_back(modality->dataType());
+    }
+
+    // Compute the number of bacthes depending on mDropLast boolean
+    mNbBatch = (mDropLast) ?
+                static_cast<std::size_t>(std::floor(mNbItems / mBatchSize)) :
+                static_cast<std::size_t>(std::ceil(mNbItems / mBatchSize));
+}
+
+std::vector<std::shared_ptr<Aidge::Tensor>> Aidge::DataProvider::readBatch() const
+{
+    AIDGE_ASSERT(mIndexBatch <= mNbBatch, "Cannot fetch more data than available in database");
+    std::size_t current_batch_size;
+    if (mIndexBatch == mNbBatch) {
+        current_batch_size = mLastBatchSize;
+    } else {
+        current_batch_size = mBatchSize;
+    }
+
+    // Create batch tensors (dimensions, backends, datatype) for each modality
+    std::vector<std::shared_ptr<Tensor>> batchTensors;
+    auto dataBatchDims = mDataDims;
+    for (std::size_t i = 0; i < mNumberModality; ++i) {
+        dataBatchDims[i].insert(dataBatchDims[i].begin(), current_batch_size);
+        auto batchData = std::make_shared<Tensor>();
+        batchData->resize(dataBatchDims[i]);
+        batchData->setBackend("cpu");
+        batchData->setDataType(mDataTypes[i]);
+        batchTensors.push_back(batchData);
+    }
+
+    // Call each database item and concatenate each data modularity in the batch tensors
+    for (std::size_t i = 0; i < current_batch_size; ++i){
+
+        auto dataItem = mDatabase.getItem(mBatches[(mIndexBatch-1)*mBatchSize+i]);
+        // auto dataItem = mDatabase.getItem(startIndex+i);
+        // assert same number of modalities
+        assert(dataItem.size() == mNumberModality && "DataProvider readBatch : item from database have inconsistent number of modality.");
+
+        // Browse each modularity in the database item
+        for (std::size_t j = 0; j < mNumberModality; ++j) {
+            auto dataSample = dataItem[j];
+
+            // Assert tensor sizes
+            assert(dataSample->dims() == mDataDims[j] && "DataProvider readBatch : corrupted Data size");
+
+            // Assert implementation backend
+            // assert(dataSample->getImpl()->backend() == mDataBackends[j] && "DataProvider readBatch : corrupted data backend");
+
+            // Assert DataType
+            assert(dataSample->dataType() == mDataTypes[j] && "DataProvider readBatch : corrupted data DataType");
+
+            // Concatenate into the batch tensor
+            batchTensors[j]->getImpl()->copy(dataSample->getImpl()->rawPtr(), dataSample->size(), i*dataSample->size());
+        }
+    }
+    return batchTensors;
+}
+
+
+void Aidge::DataProvider::setBatches(){
+
+    mBatches.clear();
+    mBatches.resize(mNbItems);
+    std::iota(mBatches.begin(),
+              mBatches.end(),
+              0U);
+
+    if (mShuffle){
+        Aidge::Random::randShuffle(mBatches);
+    }
+
+    if (mNbItems % mBatchSize !=0){ // The last batch is not full
+        std::size_t lastBatchSize = static_cast<std::size_t>(mNbItems % mBatchSize);
+        if (mDropLast){ // Remove the last non-full batch
+            AIDGE_ASSERT(lastBatchSize <= mBatches.size(), "Last batch bigger than the size of database");
+            mBatches.erase(mBatches.end() - lastBatchSize, mBatches.end());
+            mLastBatchSize = mBatchSize;
+        } else { // Keep the last non-full batch
+            mLastBatchSize = lastBatchSize;
+        }
+    } else { // The last batch is full
+        mLastBatchSize = mBatchSize;
+    }
+}
diff --git a/src/data/Tensor.cpp b/src/data/Tensor.cpp
index da0c626d78dd1cc4452bfc07bf6c6a7f58b8d1e4..b6aa4f2e50a5a3db8c3965a8e618fcf4f0299fe8 100644
--- a/src/data/Tensor.cpp
+++ b/src/data/Tensor.cpp
@@ -10,16 +10,257 @@
  ********************************************************************************/
 
 #include "aidge/data/Tensor.hpp"
-#include "aidge/utils/Types.h"
+
+#include <cstddef>
+#include <vector>
+
 #include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
+
+Aidge::Tensor& Aidge::Tensor::operator=(const Aidge::Tensor& other) {
+    if (this == &other) {
+        return *this;
+    }
+    resize(other.dims(), other.strides());
+    setDataType(other.dataType(), false); // do not convert existing data
+    if (other.hasImpl()) {
+        if (hasImpl()) {
+            copyFrom(other);
+        }
+        else {
+            // Perform a shallow copy only
+            setImpl(other.mImpl, other.mImplOffset);
+        }
+    }
+    else {
+        setImpl(nullptr);
+    }
+    return *this;
+}
+
+
+Aidge::Tensor::~Tensor() noexcept = default;
+
+
+void Aidge::Tensor::resize(const std::vector<Aidge::DimSize_t> &dims, std::vector<Aidge::DimSize_t> strides) {
+    // TODO: scalar Tensor not handled
+    if (dims.empty()) { // scalar
+        mDims = std::vector<DimSize_t>(0);
+        mStrides = std::vector<DimSize_t>({1});
+        mContiguous = true;
+
+        computeSize();
+        if (mImpl) {
+            mImpl->resize(mDims);
+        }
+        return;
+    }
+
+    bool checkContiguous = true;
+    if (strides.empty()) {
+        strides.resize(dims.size());
+        size_t expectedStride = 1;
+        for (int dim = dims.size() - 1; dim >= 0; --dim) {
+            strides[dim] = expectedStride;
+            expectedStride*= dims[dim];
+        }
+        checkContiguous = false;
+    }
+    else {
+        AIDGE_ASSERT(strides.size() == dims.size(), "Number of strides must match number of dims");
+    }
+
+    if (mImpl && mImpl.use_count() > 1) {
+        // Here we could also create a new storage for this tensor in this case
+        // But, is it more likely that the user really wants this, or that he did a mistake?
+        AIDGE_ASSERT(dims == mDims && strides == mStrides, "Cannot resize Tensor with shared storage");
+    }
+    else {
+        mDims = dims;
+        mStrides = strides;
+
+        mContiguous = true;
+        if (checkContiguous) {
+            std::size_t expectedStride = 1;
+            // std::size_t i = dims.size();
+            // while ((i-- > 0) && (strides[i] == expectedStride)) {
+            //     mContiguous&= (strides[i] == expectedStride);
+            //     expectedStride*= dims[i];
+            // }
+            for (std::size_t i = dims.size()-1; i > 0; --i) {
+                if (strides[i] != expectedStride) {
+                    mContiguous = false;
+                    break;
+                }
+                expectedStride*= dims[i];
+            }
+            mContiguous &= (strides[0] == expectedStride);
+        }
+
+        computeSize();
+        if (mImpl) {
+            mImpl->resize(mDims);
+        }
+    }
+}
+
+std::string Aidge::Tensor::toString() const {
+    AIDGE_ASSERT(mImpl && (dims().empty() || (dims() == std::vector<DimSize_t>({0})) || (mImpl->hostPtr() != nullptr)), "tensor should have a valid host pointer");
+
+    // TODO: move lambda elsewhere?
+    auto ptrToString = [](DataType dt, void* ptr, std::size_t idx) {
+        switch (dt) {
+        case DataType::Float64:
+            return std::to_string(static_cast<double*>(ptr)[idx]);
+        case DataType::Float32:
+            return std::to_string(static_cast<float*>(ptr)[idx]);
+        case DataType::Float16:
+            return std::to_string(static_cast<half_float::half*>(ptr)[idx]);
+        case DataType::Int8:
+            return std::to_string(static_cast<int8_t*>(ptr)[idx]);
+        case DataType::Int16:
+            return std::to_string(static_cast<int16_t*>(ptr)[idx]);
+        case DataType::Int32:
+            return std::to_string(static_cast<int32_t*>(ptr)[idx]);
+        case DataType::Int64:
+            return std::to_string(static_cast<int64_t*>(ptr)[idx]);
+        case DataType::UInt8:
+            return std::to_string(static_cast<uint8_t*>(ptr)[idx]);
+        case DataType::UInt16:
+            return std::to_string(static_cast<uint16_t*>(ptr)[idx]);
+        case DataType::UInt32:
+            return std::to_string(static_cast<uint32_t*>(ptr)[idx]);
+        case DataType::UInt64:
+            return std::to_string(static_cast<uint64_t*>(ptr)[idx]);
+        default:
+            AIDGE_ASSERT(true, "unsupported type to convert to string");
+        }
+        return std::string("?");  // To make Clang happy
+    };
+
+    if (dims().empty()) { return ptrToString(mDataType, mImpl->hostPtr(), 0); }
+    std::string res;
+    std::size_t dim = 0;
+    std::size_t counter = 0;
+    if (nbDims()>=2) {
+        std::vector<std::size_t> dimVals(nbDims(), 0);
+        res += "{\n";
+        while (counter < mSize) {
+            std::string spaceString = std::string((dim+1)<<1,' ');
+            if (dim < nbDims()-2) {
+                if (dimVals[dim] == 0) {
+                    res += spaceString + "{\n";
+                    ++dim;
+                } else if (dimVals[dim] < static_cast<std::size_t>(dims()[dim])) {
+                    res += spaceString + "},\n" + spaceString + "{\n";
+                    ++dim;
+                } else {
+                    res += spaceString + "}\n";
+                    dimVals[dim--] = 0;
+                    dimVals[dim]++;
+                }
+            } else {
+                for (; dimVals[dim] < static_cast<std::size_t>(dims()[dim]); ++dimVals[dim]) {
+                    res += spaceString + "{";
+                    for (DimSize_t j = 0; j < dims()[dim + 1] - 1; ++j) {
+                        res += " " + ptrToString(mDataType, mImpl->hostPtr(mImplOffset), counter++) + ",";
+                    }
+                    res += " " + ptrToString(mDataType, mImpl->hostPtr(mImplOffset), counter++) + "}";
+                    if (dimVals[dim] < static_cast<std::size_t>(dims()[dim] - 1)) {
+                        res += ",";
+                    }
+                    res += "\n";
+                }
+                if (dim == 0) {
+                    break;
+                }
+                dimVals[dim--] = 0;
+                dimVals[dim]++;
+            }
+        }
+
+        for(int i = static_cast<int>(dim); i > 0; --i) {
+            res += std::string((dim+1)<<1,' ') + "}\n";
+        }
+    } else {
+        res += "{";
+        for (DimSize_t j = 0; j < dims()[0]; ++j) {
+            res += " " + ptrToString(mDataType, mImpl->hostPtr(mImplOffset), j) + ((j < dims()[0]-1) ? "," : " ");
+        }
+    }
+    res += "}";
+    return res;
+}
+
+Aidge::Tensor Aidge::Tensor::extract(const std::vector<std::size_t>& fixedCoord) const {
+    AIDGE_ASSERT(isContiguous(), "Tensor must be contiguous");
+    AIDGE_ASSERT(fixedCoord.size() <= mDims.size(), "Number of coordinates is higher than number of dimensions");
+
+    Tensor subTensor(mDataType);
+    subTensor.resize(std::vector<size_t>(mDims.cbegin() + fixedCoord.size(), mDims.cend()),
+        std::vector<size_t>(mStrides.cbegin() + fixedCoord.size(), mStrides.cend()));
+    subTensor.setBackend(mImpl->backend(), mImpl->device().second);
+    subTensor.setImpl(mImpl, mImplOffset + getStorageIdx(fixedCoord));
+    return subTensor;
+}
+
+Aidge::Tensor Aidge::Tensor::extract(const std::vector<std::size_t>& startCoord, const std::vector<std::size_t>& dims) const {
+    AIDGE_ASSERT(isContiguous(), "Tensor must be contiguous");
+    AIDGE_ASSERT(startCoord.size() == mDims.size(), "Coordinates does not match number of dimensions");
+
+    Tensor subTensor(mDataType);
+    subTensor.resize(dims, mStrides);
+    subTensor.setBackend(mImpl->backend(), mImpl->device().second);
+    subTensor.setImpl(mImpl, mImplOffset + getStorageIdx(startCoord));
+    return subTensor;
+}
+
+void Aidge::Tensor::makeContiguous() {
+    if (!mImpl || isContiguous()) {
+        return;
+    }
+
+    // Block so that mImpl ref count is 1 for resize()
+    {
+        // Create a new storage that will be contiguous
+        std::shared_ptr<TensorImpl> newImpl = Registrar<Tensor>::create({mImpl->backend(), mDataType})(mImpl->device().second, mDims);
+        // Copy elements from old to new storage
+        std::size_t idx = 0;
+        while (idx < mSize) {
+            const std::size_t storageIdx = getStorageIdx(getCoord(idx));
+
+            // Determine the size of the contiguous chunk
+            std::size_t copySize = 1;
+            while (idx + copySize < mSize &&
+                getStorageIdx(getCoord(idx + copySize)) == storageIdx + copySize)
+            {
+                ++copySize;
+            }
+
+            // Perform a single copy for the contiguous chunk
+            newImpl->copy(mImpl->rawPtr(mImplOffset + storageIdx), copySize, idx);
+
+            // Move to the next index after the contiguous chunk
+            idx += copySize;
+        }
+        // Replace old storage by new, contiguous, storage
+        setImpl(newImpl);
+    }
+
+    // Resize tensor without strides => tensor is now contiguous
+    resize(mDims);
+}
 
 void Aidge::Tensor::copyCast(const Tensor& src) {
     if (&src == this) {
         return;
     }
 
+    AIDGE_ASSERT(src.isContiguous(), "cannot copy-cast non-contiguous tensor");
+
     // Current Tensor has necessarily a data type, but may not have backend
-    if (!getImpl()) {
+    if (!hasImpl()) {
         // If no backend was set for the current tensor, use the same as src
         const auto deviceSrc = src.getImpl()->device();
         setBackend(deviceSrc.first, deviceSrc.second);
@@ -27,7 +268,7 @@ void Aidge::Tensor::copyCast(const Tensor& src) {
     resize(src.dims());
 
     AIDGE_ASSERT(src.getImpl()->device() == getImpl()->device(), "cannot copy-cast from a different backend/device");
-    getImpl()->copyCast(src.getImpl()->rawPtr(), src.size(), src.dataType());
+    getImpl()->copyCast(src.getImpl()->rawPtr(src.mImplOffset), src.dataType(), src.size(), mImplOffset);
 }
 
 void Aidge::Tensor::copyFrom(const Tensor& src) {
@@ -35,8 +276,10 @@ void Aidge::Tensor::copyFrom(const Tensor& src) {
         return;
     }
 
+    AIDGE_ASSERT(src.isContiguous(), "cannot copy from non-contiguous tensor");
+
     // Current Tensor has necessarily a data type, but may not have backend
-    if (!getImpl()) {
+    if (!hasImpl()) {
         // If no backend was set for the current tensor, use the same as src
         const auto deviceSrc = src.getImpl()->device();
         setBackend(deviceSrc.first, deviceSrc.second);
@@ -44,7 +287,7 @@ void Aidge::Tensor::copyFrom(const Tensor& src) {
     resize(src.dims());
 
     AIDGE_ASSERT(src.dataType() == dataType(), "cannot copy from a different data type");
-    getImpl()->copyFrom(*(src.getImpl()), src.size());
+    getImpl()->copyFrom(*(src.getImpl()), src.size(), src.mImplOffset, mImplOffset);
 }
 
 void Aidge::Tensor::copyCastFrom(const Tensor& src, std::shared_ptr<Tensor>& movedSrcPtr) {
@@ -52,6 +295,8 @@ void Aidge::Tensor::copyCastFrom(const Tensor& src, std::shared_ptr<Tensor>& mov
         return;
     }
 
+    AIDGE_ASSERT(src.isContiguous(), "cannot copy-cast from non-contiguous tensor");
+
     // Current Tensor has necessarily a data type, but may not have backend
     if (!getImpl()) {
         // If no backend was set for the current tensor, use the same as src
@@ -65,12 +310,35 @@ void Aidge::Tensor::copyCastFrom(const Tensor& src, std::shared_ptr<Tensor>& mov
         const auto device = getImpl()->device();
         const Tensor& movedSrc = src.refFrom(movedSrcPtr, device.first, device.second);
         // Second, copy-cast data (necessary)
-        getImpl()->copyCast(movedSrc.getImpl()->rawPtr(), movedSrc.size(), movedSrc.dataType());
+        getImpl()->copyCast(movedSrc.getImpl()->rawPtr(movedSrc.mImplOffset), movedSrc.dataType(), movedSrc.size(), mImplOffset);
     }
     else {
         // Directly copy, no conversion necessary
         // Avoid making a double copy if both data type and device are the same
-        getImpl()->copyFrom(*(src.getImpl()), src.size());
+        getImpl()->copyFrom(*(src.getImpl()), src.size(), src.mImplOffset, mImplOffset);
+    }
+}
+
+Aidge::Tensor& Aidge::Tensor::refContiguous(std::shared_ptr<Tensor>& fallback) {
+    // Scott Meyers' solution to avoid code duplication
+    return const_cast<Tensor&>(static_cast<const Tensor&>(*this).refContiguous(fallback));
+}
+
+const Aidge::Tensor& Aidge::Tensor::refContiguous(std::shared_ptr<Tensor>& fallback) const {
+    AIDGE_ASSERT(getImpl(), "no backend was set for tensor, cannot refCast() it");
+
+    if (isContiguous()) {
+        return *this;
+    }
+    else {
+        if (this != fallback.get()) {
+            // Shallow copy to fallback
+            *fallback = *this;
+        }
+
+        // Make fallback contiguous
+        fallback->makeContiguous();
+        return *fallback;
     }
 }
 
@@ -91,6 +359,8 @@ const Aidge::Tensor& Aidge::Tensor::refCast(std::shared_ptr<Tensor>& fallback, c
             fallback->setDataType(dt);
         }
         else {
+            AIDGE_ASSERT(isContiguous(), "cannot refCast non-contiguous tensor");
+
             if (!fallback) {
                 fallback = std::make_shared<Tensor>(dt);
             }
@@ -101,7 +371,7 @@ const Aidge::Tensor& Aidge::Tensor::refCast(std::shared_ptr<Tensor>& fallback, c
             const auto device = getImpl()->device();
             fallback->setBackend(device.first, device.second, false); // don't keep previous data (no copy)
             fallback->resize(dims());
-            fallback->getImpl()->copyCast(getImpl()->rawPtr(), size(), dataType());
+            fallback->getImpl()->copyCast(getImpl()->rawPtr(mImplOffset), dataType(), size(), fallback->mImplOffset);
         }
         return *fallback;
     }
@@ -124,6 +394,8 @@ const Aidge::Tensor& Aidge::Tensor::refFrom(std::shared_ptr<Tensor>& fallback, c
             fallback->setBackend(backend, device);
         }
         else {
+            AIDGE_ASSERT(isContiguous(), "cannot refFrom non-contiguous tensor");
+
             if (!fallback) {
                 fallback = std::make_shared<Tensor>(dataType());
             }
@@ -133,8 +405,41 @@ const Aidge::Tensor& Aidge::Tensor::refFrom(std::shared_ptr<Tensor>& fallback, c
 
             fallback->setBackend(backend, device, false); // don't keep previous data (no copy)
             fallback->resize(dims());
-            fallback->getImpl()->copyFrom(*getImpl(), size());
+            fallback->getImpl()->copyFrom(*getImpl(), size(), mImplOffset, fallback->mImplOffset);
+        }
+        return *fallback;
+    }
+}
+
+Aidge::Tensor& Aidge::Tensor::ref(std::shared_ptr<Tensor>& fallback, const Aidge::DataType& dt, const std::string &backend, DeviceIdx_t device) {
+    // Scott Meyers' solution to avoid code duplication
+    return const_cast<Tensor&>(static_cast<const Tensor&>(*this).ref(fallback, dt, backend, device));
+}
+
+const Aidge::Tensor& Aidge::Tensor::ref(std::shared_ptr<Tensor>& fallback, const Aidge::DataType& dt, const std::string &backend, DeviceIdx_t device) const {
+    AIDGE_ASSERT(getImpl(), "no backend was set for tensor, cannot ref() it");
+
+    if (dt == dataType() && std::make_pair(backend, device) == getImpl()->device()) {
+        return *this;
+    }
+    else {
+        // Change fallback type, backend & device, without any data copy
+        if (!fallback) {
+            fallback = std::make_shared<Tensor>(dt);
+        }
+        else {
+            fallback->setDataType(dt, false); // don't keep previous data (no copy)
         }
+
+        fallback->setBackend(backend, device, false); // don't keep previous data (no copy)
+        fallback->resize(dims());
         return *fallback;
     }
 }
+
+std::set<std::string> Aidge::Tensor::getAvailableBackends() {
+    std::set<std::string> backendsList;
+    for(const auto& tupleKey : Registrar<Tensor>::getKeys())
+        backendsList.insert(std::get<0>(tupleKey));
+    return backendsList;
+}
diff --git a/src/filler/ConstantFiller.cpp b/src/filler/ConstantFiller.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1e992f4a192c2fd629b2c813c902b127f29a2b02
--- /dev/null
+++ b/src/filler/ConstantFiller.cpp
@@ -0,0 +1,44 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include "aidge/filler/Filler.hpp"
+
+#include <cstddef>  // std::size_t
+#include <memory>
+#include <string>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+
+
+template<typename T>
+void Aidge::constantFiller(std::shared_ptr<Aidge::Tensor> tensor, T constantValue) {
+    AIDGE_ASSERT(tensor->getImpl(),
+                 "Tensor got no implementation, cannot fill it.");
+    AIDGE_ASSERT(NativeType<T>::type == tensor->dataType(), "Wrong data type");
+
+    std::shared_ptr<Aidge::Tensor> cpyTensor;
+    // Create cpy only if tensor not on CPU
+    Aidge::Tensor& tensorWithValues =
+        tensor->refCastFrom(cpyTensor, tensor->dataType(), "cpu");
+
+    // Setting values
+    for (std::size_t idx = 0; idx < tensorWithValues.size(); ++idx) {
+        tensorWithValues.set<T>(idx, constantValue);
+    }
+
+    // Copy values back to the original tensors (actual copy only if needed)
+    tensor->copyCastFrom(tensorWithValues);
+}
+
+
+template void Aidge::constantFiller<float>(std::shared_ptr<Aidge::Tensor>, float);
+template void Aidge::constantFiller<double>(std::shared_ptr<Aidge::Tensor>, double);
diff --git a/src/filler/Filler.cpp b/src/filler/Filler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..34e04c2ba84ad493429bceadd54f4fa27df69bcd
--- /dev/null
+++ b/src/filler/Filler.cpp
@@ -0,0 +1,40 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include "aidge/filler/Filler.hpp"
+
+#include <cstdint>  // std::uint32_t
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Types.h"
+
+
+void Aidge::calculateFanInFanOut(std::shared_ptr<Aidge::Tensor> tensor,
+                                 std::uint32_t& fanIn, std::uint32_t& fanOut) {
+    AIDGE_ASSERT(
+        tensor->nbDims() == 4,
+        "Tensor need to have 4 dimensions to compute FanIn and FanOut.");
+    // Warning: This function suppose NCXX data layout.
+    // Aidge currently only support NCHW but this maybe not be true in the
+    // future.
+    DimSize_t batchSize = tensor->dims()[0];
+    DimSize_t channelSize = tensor->dims()[1];
+    AIDGE_ASSERT(batchSize != 0,
+                 "Cannot calculate FanIn if tensor batch size is 0.");
+    AIDGE_ASSERT(channelSize != 0,
+                 "Cannot calculate FanOut if tensor channel size is 0.");
+    fanIn =  static_cast<std::uint32_t>(tensor->size() / batchSize);
+    fanOut = static_cast<std::uint32_t>(tensor->size() / channelSize);
+}
diff --git a/src/filler/HeFiller.cpp b/src/filler/HeFiller.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..74d681f1a05c15045d27a0fe678aa676d16af077
--- /dev/null
+++ b/src/filler/HeFiller.cpp
@@ -0,0 +1,59 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+#include <memory>
+#include <random>  // normal_distribution, uniform_real_distribution
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/filler/Filler.hpp"
+#include "aidge/utils/Random.hpp"
+
+template <typename T>
+void Aidge::heFiller(std::shared_ptr<Aidge::Tensor> tensor,
+                     Aidge::VarianceNorm varianceNorm, T meanNorm, T scaling) {
+    AIDGE_ASSERT(tensor->getImpl(),
+                 "Tensor got no implementation, cannot fill it.");
+    AIDGE_ASSERT(NativeType<T>::type == tensor->dataType(), "Wrong data type");
+
+    unsigned int fanIn, fanOut = 0;
+    Aidge::calculateFanInFanOut(tensor, fanIn, fanOut);
+
+    const T n((varianceNorm == Aidge::VarianceNorm::FanIn) ? fanIn
+              : (varianceNorm == Aidge::VarianceNorm::Average)
+                  ? (fanIn + fanOut) / 2.0
+                  : fanOut);
+
+    const T stdDev(std::sqrt(2.0 / n));
+
+    const T mean(varianceNorm == Aidge::VarianceNorm::FanIn ? meanNorm / fanIn
+                 : (varianceNorm == Aidge::VarianceNorm::Average)
+                     ? meanNorm / ((fanIn + fanOut) / 2.0)
+                     : meanNorm / fanOut);
+
+    std::normal_distribution<T> normalDist(mean, stdDev);
+
+    std::shared_ptr<Tensor> cpyTensor;
+    // Create cpy only if tensor not on CPU
+    Tensor& tensorWithValues =
+        tensor->refCastFrom(cpyTensor, tensor->dataType(), "cpu");
+
+    // Setting values
+    for (std::size_t idx = 0; idx < tensorWithValues.size(); ++idx) {
+        tensorWithValues.set<T>(idx, scaling*normalDist(Aidge::Random::Generator::get()));
+    }
+
+    // Copy values back to the original tensors (actual copy only if needed)
+    tensor->copyCastFrom(tensorWithValues);
+}
+
+template void Aidge::heFiller<float>(std::shared_ptr<Aidge::Tensor>,
+                                     Aidge::VarianceNorm, float, float);
+template void Aidge::heFiller<double>(std::shared_ptr<Aidge::Tensor>,
+                                      Aidge::VarianceNorm, double, double);
diff --git a/src/filler/NormalFiller.cpp b/src/filler/NormalFiller.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f30b32431cf466b10c1b10df8e0e5ccec9f483b6
--- /dev/null
+++ b/src/filler/NormalFiller.cpp
@@ -0,0 +1,44 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+#include <memory>
+#include <random>  // normal_distribution, uniform_real_distribution
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/filler/Filler.hpp"
+#include "aidge/utils/Random.hpp"
+
+template <typename T>
+void Aidge::normalFiller(std::shared_ptr<Aidge::Tensor> tensor, double mean,
+                         double stdDev) {
+    AIDGE_ASSERT(tensor->getImpl(),
+                 "Tensor got no implementation, cannot fill it.");
+    AIDGE_ASSERT(NativeType<T>::type == tensor->dataType(), "Wrong data type");
+
+    std::normal_distribution<T> normalDist(mean, stdDev);
+
+    std::shared_ptr<Tensor> cpyTensor;
+    // Create cpy only if tensor not on CPU
+    Tensor& tensorWithValues =
+        tensor->refCastFrom(cpyTensor, tensor->dataType(), "cpu");
+
+    // Setting values
+    for (std::size_t idx = 0; idx < tensorWithValues.size(); ++idx) {
+        tensorWithValues.set<T>(idx, normalDist(Aidge::Random::Generator::get()));
+    }
+
+    // Copy values back to the original tensors (actual copy only if needed)
+    tensor->copyCastFrom(tensorWithValues);
+}
+
+template void Aidge::normalFiller<float>(std::shared_ptr<Aidge::Tensor>, double,
+                                         double);
+template void Aidge::normalFiller<double>(std::shared_ptr<Aidge::Tensor>,
+                                          double, double);
diff --git a/src/filler/UniformFiller.cpp b/src/filler/UniformFiller.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a942f59d717fd8d7b541ee28868a7fb9f2e7cd95
--- /dev/null
+++ b/src/filler/UniformFiller.cpp
@@ -0,0 +1,44 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+#include <memory>
+#include <random>  // normal_distribution, uniform_real_distribution
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/filler/Filler.hpp"
+#include "aidge/utils/Random.hpp"
+
+template <typename T>
+void Aidge::uniformFiller(std::shared_ptr<Aidge::Tensor> tensor, T min, T max) {
+    AIDGE_ASSERT(tensor->getImpl(),
+                 "Tensor got no implementation, cannot fill it.");
+    AIDGE_ASSERT(NativeType<T>::type == tensor->dataType(), "Wrong data type");
+
+
+    std::uniform_real_distribution<T> uniformDist(min, max);
+
+    std::shared_ptr<Aidge::Tensor> cpyTensor;
+    // Create cpy only if tensor not on CPU
+    Aidge::Tensor& tensorWithValues =
+        tensor->refCastFrom(cpyTensor, tensor->dataType(), "cpu");
+
+    // Setting values
+    for (std::size_t idx = 0; idx < tensorWithValues.size(); ++idx) {
+        tensorWithValues.set<T>(idx, uniformDist(Aidge::Random::Generator::get()));
+    }
+
+    // Copy values back to the original tensors (actual copy only if needed)
+    tensor->copyCastFrom(tensorWithValues);
+}
+
+template void Aidge::uniformFiller<float>(std::shared_ptr<Aidge::Tensor>, float,
+                                          float);
+template void Aidge::uniformFiller<double>(std::shared_ptr<Aidge::Tensor>,
+                                           double, double);
diff --git a/src/filler/XavierFiller.cpp b/src/filler/XavierFiller.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a1de15971ca8063e504e270fa6d2275d93270460
--- /dev/null
+++ b/src/filler/XavierFiller.cpp
@@ -0,0 +1,90 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+#include <memory>
+#include <random>  // normal_distribution, uniform_real_distribution
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/filler/Filler.hpp"
+#include "aidge/utils/Random.hpp"
+
+template <typename T>
+void Aidge::xavierUniformFiller(std::shared_ptr<Aidge::Tensor> tensor,
+                                T scaling, Aidge::VarianceNorm varianceNorm) {
+    AIDGE_ASSERT(tensor->getImpl(),
+                 "Tensor got no implementation, cannot fill it.");
+    AIDGE_ASSERT(NativeType<T>::type == tensor->dataType(), "Wrong data type");
+
+    unsigned int fanIn, fanOut = 0;
+    Aidge::calculateFanInFanOut(tensor, fanIn, fanOut);
+
+    const T n((varianceNorm == Aidge::VarianceNorm::FanIn) ? fanIn
+              : (varianceNorm == Aidge::VarianceNorm::Average)
+                  ? (fanIn + fanOut) / 2.0
+                  : fanOut);
+    const T scale(std::sqrt(3.0 / n));
+
+    std::uniform_real_distribution<T> uniformDist(-scale, scale);
+
+    std::shared_ptr<Aidge::Tensor> cpyTensor;
+    // Create cpy only if tensor not on CPU
+    Aidge::Tensor& tensorWithValues =
+        tensor->refCastFrom(cpyTensor, tensor->dataType(), "cpu");
+    // Setting values
+    for (std::size_t idx = 0; idx < tensorWithValues.size(); ++idx) {
+        tensorWithValues.set<T>(
+            idx, scaling * uniformDist(Aidge::Random::Generator::get()));
+    }
+
+    // Copy values back to the original tensors (actual copy only if needed)
+    tensor->copyCastFrom(tensorWithValues);
+}
+template <typename T>
+void Aidge::xavierNormalFiller(std::shared_ptr<Aidge::Tensor> tensor, T scaling,
+                               Aidge::VarianceNorm varianceNorm) {
+    AIDGE_ASSERT(tensor->getImpl(),
+                 "Tensor got no implementation, cannot fill it.");
+    AIDGE_ASSERT(NativeType<T>::type == tensor->dataType(), "Wrong data type");
+
+    unsigned int fanIn, fanOut = 0;
+    Aidge::calculateFanInFanOut(tensor, fanIn, fanOut);
+
+    const T n((varianceNorm == Aidge::VarianceNorm::FanIn) ? fanIn
+              : (varianceNorm == Aidge::VarianceNorm::Average)
+                  ? (fanIn + fanOut) / 2.0
+                  : fanOut);
+    const double stdDev(std::sqrt(1.0 / n));
+
+    std::normal_distribution<T> normalDist(0.0, stdDev);
+
+    std::shared_ptr<Aidge::Tensor> cpyTensor;
+    // Create cpy only if tensor not on CPU
+    Aidge::Tensor& tensorWithValues =
+        tensor->refCastFrom(cpyTensor, tensor->dataType(), "cpu");
+
+    // Setting values
+    for (std::size_t idx = 0; idx < tensorWithValues.size(); ++idx) {
+        tensorWithValues.set<T>(
+            idx, scaling * normalDist(Aidge::Random::Generator::get()));
+    }
+
+    // Copy values back to the original tensors (actual copy only if needed)
+    tensor->copyCastFrom(tensorWithValues);
+}
+
+template void Aidge::xavierUniformFiller<float>(std::shared_ptr<Aidge::Tensor>,
+                                                float, Aidge::VarianceNorm);
+template void Aidge::xavierUniformFiller<double>(std::shared_ptr<Aidge::Tensor>,
+                                                 double, Aidge::VarianceNorm);
+
+template void Aidge::xavierNormalFiller<float>(std::shared_ptr<Aidge::Tensor>,
+                                               float, Aidge::VarianceNorm);
+template void Aidge::xavierNormalFiller<double>(std::shared_ptr<Aidge::Tensor>,
+                                                double, Aidge::VarianceNorm);
diff --git a/src/graph/GraphView.cpp b/src/graph/GraphView.cpp
index 968e98e75cc587977eb3033fe7f25936880755a4..dcd7a06ef8560ad6d4a572cd823e2f9dc357b73c 100644
--- a/src/graph/GraphView.cpp
+++ b/src/graph/GraphView.cpp
@@ -9,17 +9,36 @@
  *
  ********************************************************************************/
 
-#include <algorithm>
+#include "aidge/graph/GraphView.hpp"
+
+#include <algorithm>     // std::find, std::set_intersection, std::transform
 #include <cassert>
-#include <iterator>
-#include <utility>
-#include <numeric>
+#include <stdexcept>     // std::runtime_error
+#include <cstddef>       // std::size_t
+#include <cstdio>        // std::fclose, std::fopen
+#include <fmt/format.h>
+#include <iterator>      // std::back_inserter, std::distance, std::inserter,
+                         // std::next
+#include <map>
+#include <memory>        // std::dynamic_pointer_cast, std::static_pointer_cast
+#include <set>
+#include <string>        // std::to_string
+#include <utility>       // std::make_pair, std::pair
+#include <vector>
 
-#include "aidge/utils/Types.h"
-#include "aidge/graph/GraphView.hpp"
 #include "aidge/data/Tensor.hpp"
+#include "aidge/operator/GenericOperator.hpp"
+#include "aidge/operator/MetaOperator.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/operator/Producer.hpp"
+#include "aidge/utils/Directories.hpp"
 #include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Types.h"
+
+
+const std::shared_ptr<Aidge::Node> Aidge::GraphView::operator[](const std::string& nodeName) const {
+    return (mNodeRegistry.find(nodeName) != mNodeRegistry.cend()) ? mNodeRegistry.at(nodeName) : nullptr;
+}
 
 ///////////////////////////////////////////////////////
 //        FUNCTIONAL DESCRIPTION
@@ -50,43 +69,59 @@ Aidge::Connector Aidge::GraphView::operator()(
 //        INNER
 ///////////////////////////////////////////////////////
 
-std::string Aidge::GraphView::name() const { return mName; }
+bool Aidge::GraphView::inView(const std::shared_ptr<Aidge::Node>& nodePtr) const {
+    return mNodes.find(nodePtr) != mNodes.cend();
+}
 
-void Aidge::GraphView::setName(const std::string &name) { mName = name; }
 
+void Aidge::GraphView::save(const std::string& path, bool verbose, bool showProducers) const {
+    auto fp = std::unique_ptr<FILE, decltype(&std::fclose)>(std::fopen((path + ".mmd").c_str(), "w"), &std::fclose);
 
-void Aidge::GraphView::save(std::string path, bool verbose, bool showProducers) const {
-    FILE *fp = std::fopen((path + ".mmd").c_str(), "w");
-    std::fprintf(fp,
-                "%%%%{init: {'flowchart': { 'curve': 'monotoneY'}, "
-                "'fontFamily': 'Verdana' } }%%%%\nflowchart TB\n\n");
+    if (!fp) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error,
+            "Could not create graph view log file: {}", path + ".mmd");
+    }
 
-    std::map<const std::string, std::size_t> typeCounter;
-    std::map<std::shared_ptr<Node>, std::string> namePtrTable;
+    fmt::print(fp.get(),
+                "%%{{init: {{'flowchart': {{ 'curve': 'monotoneY'}}, "
+                "'fontFamily': 'Verdana' }} }}%%\nflowchart TB\n\n");
 
     // Start by creating every node
-    for (const std::shared_ptr<Node> &node_ptr : mNodes) {
-        const std::string currentType = node_ptr->type();
-        if (typeCounter.find(currentType) == typeCounter.end())
-            typeCounter[currentType] = 0;
-        ++typeCounter[currentType];
+    const auto namePtrTable = getRankedNodesName("{3}");
 
+    for (const std::shared_ptr<Node> &node_ptr : mNodes) {
         std::string givenName =
             (node_ptr->name().empty())
-                ? "<em>" + currentType + "#" + std::to_string(typeCounter[currentType]) + "</em>"
-                : "\"" + node_ptr->name() + "\\n<sub><em>( " + currentType + "#" + std::to_string(typeCounter[currentType]) + " )</em></sub>\"";
-        namePtrTable[node_ptr] =
-            (currentType + "_" + std::to_string(typeCounter[currentType]));
+                ? "<em>" + node_ptr->type() + "#" + namePtrTable.at(node_ptr) + "</em>"
+                : "\"" + node_ptr->name() + "\\n<sub><em>(" + node_ptr->type() + "#" + namePtrTable.at(node_ptr) + ")</em></sub>\"";
+
+        std::string nodeCls = "";
+        if (node_ptr->type() == "Producer") {
+          nodeCls = ":::producerCls";
+        }
+        else if (std::dynamic_pointer_cast<GenericOperator_Op>(node_ptr->getOperator())) {
+          nodeCls = ":::genericCls";
+        }
+        else if (const auto metaOp = std::dynamic_pointer_cast<MetaOperator_Op>(node_ptr->getOperator())) {
+          nodeCls = ":::metaCls";
+
+          if (verbose) {
+            metaOp->getMicroGraph()->save(path + "_" + node_ptr->type() + "#" + namePtrTable.at(node_ptr), verbose, showProducers);
+          }
+        }
 
         if (node_ptr == mRootNode) {
-          std::fprintf(fp, "%s(%s):::rootCls\n", namePtrTable[node_ptr].c_str(),
-                      givenName.c_str());
+          if (nodeCls.empty()) {
+            nodeCls = ":::rootCls";
+          }
+          else {
+            nodeCls += "_rootCls";
+          }
         }
-        else {
-            if ((currentType != "Producer") || showProducers) {
-                std::fprintf(fp, "%s(%s)\n", namePtrTable[node_ptr].c_str(),
-                            givenName.c_str());
-            }
+
+        if (node_ptr == mRootNode || node_ptr->type() != "Producer" || showProducers) {
+          fmt::print(fp.get(), "{}_{}({}){}\n", node_ptr->type(), namePtrTable.at(node_ptr),
+                      givenName, nodeCls);
         }
     }
 
@@ -96,19 +131,26 @@ void Aidge::GraphView::save(std::string path, bool verbose, bool showProducers)
         continue;
       }
       IOIndex_t outputIdx = 0;
-      for (auto childs : node_ptr->getOrderedChildren()) {
-        for (auto child : childs) {
+      for (const auto& childs : node_ptr->getOrderedChildren()) {
+        for (const auto& child : childs) {
           if (child != nullptr) {
             IOIndex_t inputIdx = 0;
             for (auto parent : child->inputs()) {
               if (parent.first == node_ptr && parent.second == outputIdx) {
+                // Add-on to display the operator's output dimensions
+                std::string dims = "";
+                const auto op = std::dynamic_pointer_cast<OperatorTensor>(node_ptr->getOperator());
+                if (op && !op->getOutput(outputIdx)->dims().empty()) {
+                  dims += " " + fmt::format("{}", op->getOutput(outputIdx)->dims());
+                }
+
                 if (mNodes.find(child) != mNodes.end()) {
-                  std::fprintf(fp, "%s-->|%u&rarr;%u|%s\n", namePtrTable[node_ptr].c_str(),
-                              outputIdx, inputIdx, namePtrTable[child].c_str());
+                  fmt::print(fp.get(), "{}_{}-->|\"{}{}&rarr;{}\"|{}_{}\n", node_ptr->type(), namePtrTable.at(node_ptr),
+                              outputIdx, dims, inputIdx, child->type(), namePtrTable.at(child));
                 }
                 else if (verbose) {
-                  std::fprintf(fp, "%s-->|%u&rarr;%u|%p:::externalCls\n", namePtrTable[node_ptr].c_str(),
-                              outputIdx, inputIdx, static_cast<void*>(child.get()));
+                  fmt::print(fp.get(), "{}_{}-->|\"{}{}&rarr;{}\"|{}:::externalCls\n", node_ptr->type(), namePtrTable.at(node_ptr),
+                              outputIdx, dims, inputIdx, static_cast<void*>(child.get()));
                 }
                 break;
               }
@@ -122,62 +164,146 @@ void Aidge::GraphView::save(std::string path, bool verbose, bool showProducers)
 
     size_t inputIdx = 0;
     for (auto input : mInputNodes) {
-      std::fprintf(fp, "input%lu((in#%lu)):::inputCls--->|&rarr;%u|%s\n", inputIdx, inputIdx,
-                  input.second, namePtrTable[input.first].c_str());
+      if (input.first != nullptr) {
+        fmt::print(fp.get(), "input{}((in#{})):::inputCls--->|\"&rarr;{}\"|{}_{}\n", inputIdx, inputIdx,
+                    input.second, input.first->type(), namePtrTable.at(input.first));
+      }
+      else {
+        fmt::print(fp.get(), "input{}((in#{})):::inputCls\n", inputIdx, inputIdx);
+      }
       ++inputIdx;
     }
 
     size_t outputIdx = 0;
     for (auto output : mOutputNodes) {
-      std::fprintf(fp, "%s--->|%u&rarr;|output%lu((out#%lu)):::outputCls\n",
-                   namePtrTable[output.first].c_str(), output.second,
-                   outputIdx, outputIdx);
+      if (output.first != nullptr) {
+        // Add-on to display the operator's output dimensions
+        std::string dims = "";
+        const auto op = std::dynamic_pointer_cast<OperatorTensor>(output.first->getOperator());
+        if (op && op->getOutput(output.second) && !op->getOutput(output.second)->dims().empty()) {
+          dims += " " + fmt::format("{}", op->getOutput(output.second)->dims());
+        }
+
+        fmt::print(fp.get(), "{}_{}--->|\"{}{}&rarr;\"|output{}((out#{})):::outputCls\n",
+                    output.first->type(), namePtrTable.at(output.first), output.second,
+                    dims, outputIdx, outputIdx);
+      }
+      else {
+        fmt::print(fp.get(), "output{}((out#{})):::outputCls\n", outputIdx, outputIdx);
+      }
       ++outputIdx;
     }
 
-    std::fprintf(fp, "classDef inputCls fill:#afa\n");
-    std::fprintf(fp, "classDef outputCls fill:#ffa\n");
-    std::fprintf(fp, "classDef externalCls fill:#ccc\n");
-    std::fprintf(fp, "classDef rootCls stroke:#f00\n");
+    fmt::print(fp.get(), "classDef inputCls fill:#afa\n");
+    fmt::print(fp.get(), "classDef outputCls fill:#ffa\n");
+    fmt::print(fp.get(), "classDef externalCls fill:#ccc\n");
+    fmt::print(fp.get(), "classDef producerCls fill:#ccf\n");
+    fmt::print(fp.get(), "classDef genericCls fill:#f9f9ff,stroke-width:1px,stroke-dasharray: 5 5\n");
+    fmt::print(fp.get(), "classDef metaCls stroke-width:5px\n");
+    fmt::print(fp.get(), "classDef rootCls stroke:#f00\n");
+    fmt::print(fp.get(), "classDef producerCls_rootCls stroke:#f00,fill:#ccf\n");
+    fmt::print(fp.get(), "classDef genericCls_rootCls stroke:#f00,fill:#f9f9ff,stroke-width:1px,stroke-dasharray: 5 5\n");
+    fmt::print(fp.get(), "classDef metaCls_rootCls stroke:#f00,stroke-width:5px\n");
+    fmt::print(fp.get(), "\n");
+}
+
+void Aidge::GraphView::logOutputs(const std::string& dirName) const {
+  if (!Aidge::createDirectories(dirName)){
+    AIDGE_THROW_OR_ABORT(std::runtime_error, "Failed to create directory: {}.", dirName);
+  }
+  for (std::shared_ptr<Node> nodePtr : getNodes()) {
+
+    const std::string& nodePath = dirName + "/" + Aidge::filePath(nodePtr->name()) +"/";
+    if (!Aidge::createDirectories(nodePath)){
+      AIDGE_THROW_OR_ABORT(std::runtime_error, "Failed to create directory: {}.", nodePath);
+    }
 
-    if (verbose) {
-      for (const auto &c : typeCounter) {
-        std::printf("%s - %zu\n", c.first.c_str(), c.second);
+    for (IOIndex_t outIdx = 0; outIdx < nodePtr->nbOutputs(); ++outIdx) {
+      const std::string& inputPath = nodePath +"output_" + std::to_string(outIdx) + ".log";
+      auto fp = std::unique_ptr<FILE, decltype(&std::fclose)>(std::fopen(inputPath.c_str(), "w"), &std::fclose);
+      if (!fp) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error,
+            "Could not create graph view log file: {}", inputPath);
       }
+      fmt::print(fp.get(), "{}\n", nodePtr->getOperator()->getRawOutput(outIdx)->toString().c_str());
     }
+  }
+}
 
-    std::fprintf(fp, "\n");
-    std::fclose(fp);
+void Aidge::GraphView::setRootNode(NodePtr node) {
+  AIDGE_ASSERT(mNodes.find(node) != mNodes.end(), "Root node is not in the GraphView!");
+  mRootNode = node;
 }
 
 ///////////////////////////////////////////////////////
 //        TENSOR MANAGEMENT
 ///////////////////////////////////////////////////////
 
-void Aidge::GraphView::setOrderedInputs(const std::vector<std::pair<NodePtr, IOIndex_t>>& inputs) {
-  AIDGE_ASSERT(inputs.size() <= mInputNodes.size(), "too many specified number of inputs");
+std::set<std::shared_ptr<Aidge::Node>> Aidge::GraphView::inputNodes() const {
+    std::set<std::shared_ptr<Aidge::Node>> nodes;
+    for (const auto& node : mInputNodes) {
+        nodes.insert(node.first);
+    }
+    return nodes;
+}
+
+std::set<std::shared_ptr<Aidge::Node>> Aidge::GraphView::outputNodes() const {
+    std::set<std::shared_ptr<Aidge::Node>> nodes;
+    for (const auto& node : mOutputNodes) {
+        nodes.insert(node.first);
+    }
+    return nodes;
+}
 
+bool Aidge::GraphView::isInputNode(const std::shared_ptr<Aidge::Node>& nodePtr) const {
+    const auto nodes = inputNodes();
+    return (nodes.find(nodePtr) != nodes.cend());
+}
+
+bool Aidge::GraphView::isOutputNode(const std::shared_ptr<Aidge::Node>& nodePtr) const {
+    const auto nodes = outputNodes();
+    return (nodes.find(nodePtr) != nodes.cend());
+}
+
+
+void Aidge::GraphView::setOrderedInputs(const std::vector<std::pair<NodePtr, IOIndex_t>>& inputs) {
+  size_t nbInputs = 0;
   std::vector<std::pair<NodePtr, IOIndex_t>> ignoredInputs(mInputNodes);
   for (auto input : inputs) {
-    auto it = std::find(ignoredInputs.begin(), ignoredInputs.end(), input);
-    AIDGE_ASSERT(it != ignoredInputs.end(), "unknown or duplicate input");
-    ignoredInputs.erase(it);
+    // Allow to specify dummy inputs (nullptr), but this will only be reflected
+    // in mInputNodes. All other functions (nbInputs(), inputs()) will not take
+    // it into account.
+    if (input.first != nullptr) {
+      auto it = std::find(ignoredInputs.begin(), ignoredInputs.end(), input);
+      AIDGE_ASSERT(it != ignoredInputs.end(), "unknown or duplicate input");
+      ignoredInputs.erase(it);
+      ++nbInputs;
+    }
   }
 
+  AIDGE_ASSERT(nbInputs <= mInputNodes.size(), "too many specified number of inputs");
+
   mInputNodes = inputs;
   mInputNodes.insert(mInputNodes.end(), ignoredInputs.begin(), ignoredInputs.end());
 }
 
 void Aidge::GraphView::setOrderedOutputs(const std::vector<std::pair<NodePtr, IOIndex_t>>& outputs) {
-  AIDGE_ASSERT(outputs.size() <= mOutputNodes.size(), "too many specified number of outputs");
-
+  size_t nbOutputs = 0;
   std::vector<std::pair<NodePtr, IOIndex_t>> ignoredOutputs(mOutputNodes);
   for (auto output : outputs) {
-    auto it = std::find(ignoredOutputs.begin(), ignoredOutputs.end(), output);
-    AIDGE_ASSERT(it != ignoredOutputs.end(), "unknown or duplicate output");
-    ignoredOutputs.erase(it);
+    // Allow to specify dummy outputs (nullptr), but this will only be reflected
+    // in mOutputNodes. All other functions (nbOutputs(), outputs()) will not take
+    // it into account.
+    if (output.first != nullptr) {
+      auto it = std::find(ignoredOutputs.begin(), ignoredOutputs.end(), output);
+      AIDGE_ASSERT(it != ignoredOutputs.end(), "unknown or duplicate output");
+      ignoredOutputs.erase(it);
+      ++nbOutputs;
+    }
   }
 
+  AIDGE_ASSERT(nbOutputs <= mOutputNodes.size(), "too many specified number of outputs");
+
   mOutputNodes = outputs;
   mOutputNodes.insert(mOutputNodes.end(), ignoredOutputs.begin(), ignoredOutputs.end());
 }
@@ -248,11 +374,11 @@ Aidge::GraphView::inputs() const {
 
 
 std::vector<std::pair<std::shared_ptr<Aidge::Node>, Aidge::IOIndex_t>>
-Aidge::GraphView::inputs(std::string name) const {
+Aidge::GraphView::inputs(const std::string& name) const {
   return mNodeRegistry.at(name)->inputs();
 }
 
-void Aidge::GraphView::compile(const std::string& backend, const Aidge::DataType datatype, DeviceIdx_t device) {
+void Aidge::GraphView::compile(const std::string& backend, const Aidge::DataType datatype, DeviceIdx_t device, const std::vector<std::vector<DimSize_t>> dims) {
     // Backend
     // TODO: add Backend attribute to Operator
     setBackend(backend, device);
@@ -262,13 +388,22 @@ void Aidge::GraphView::compile(const std::string& backend, const Aidge::DataType
     // Data Format
     // TODO: check actual parent output data format and the needed one. Add a Transpose Operator if necessary
     // Forward dimensions
-    forwardDims();
+    forwardDims(dims);
 }
 
-void Aidge::GraphView::forwardDims() {
+void Aidge::GraphView::forwardDims(const std::vector<std::vector<Aidge::DimSize_t>> dims) {
     // setInputs
     // Link every tensor to the right pointer
     // following parent - children informations
+    if (!dims.empty()){
+      AIDGE_ASSERT(dims.size() == mInputNodes.size(), "GraphView forwardDims error - Inconsistent number of given dimensions ({}) and graph inputs ({})", dims.size(), mInputNodes.size());
+      for (std::size_t i = 0; i < dims.size(); ++i){
+        auto tensor = std::make_shared<Tensor>(dims[i]);
+        mInputNodes[i].first->getOperator()->setInput(mInputNodes[i].second, tensor);
+      }
+    }
+
+    // Ensure every node in the graph is correctly connected
     for (std::shared_ptr<Node> nodePtr : getNodes()) {
         for (IOIndex_t i = 0; i < nodePtr->nbInputs(); ++i) {
             // assess if the input was not already set and is a Tensor then link it to parent output
@@ -280,58 +415,57 @@ void Aidge::GraphView::forwardDims() {
                         nodePtr->getOperator()->associateInput(i, inputI.first->getOperator()->getRawOutput(inputI.second));
                     }
                     else {
-                        assert(false && "Non-tensor entries not handled yet.\n");
+                        AIDGE_ASSERT(false, "Non-tensor entries not handled yet, for node {} (of type {}).", nodePtr->name(), nodePtr->type());
                     }
                 }
             } else {
-                assert(!std::static_pointer_cast<Tensor>(nodePtr->getOperator()->getRawInput(i))->empty());
+                AIDGE_ASSERT(nodePtr->getOperator()->getRawInput(i)
+                    && !std::static_pointer_cast<Tensor>(nodePtr->getOperator()->getRawInput(i))->empty(),
+                  "Missing input#{} for node {} ({})", i, nodePtr->name(), nodePtr->type());
             }
 
         }
     }
-    // Compute dimensions of every node
-    _forwardDims(inputNodes());
-}
 
-void Aidge::GraphView::_forwardDims(std::set<std::shared_ptr<Node>> listNodes) {
-    // TODO: support multi-inputs/outputs
-    std::set<std::shared_ptr<Node>> nextList = std::set<std::shared_ptr<Node>>();
-    for (std::shared_ptr<Node> nodePtr : listNodes) {
-        if (nodePtr->getOperator()->operatorType() == OperatorType::Tensor) {
-            const auto op = std::static_pointer_cast<OperatorTensor>(nodePtr->getOperator());
-            if (!op->outputDimsForwarded()) {
-                op->computeOutputDims();
-            }
-            if (!op->outputDimsForwarded()) { // try to compute output dimensions again later
-                nextList.insert(nodePtr);
-            } else { // compute output dimensions of children
-                std::set<std::shared_ptr<Node>> children = nodePtr->getChildren();
-                nextList.insert(children.begin(), children.end());
-            }
-        }
-    }
-    if (nextList.empty()) {
-        for (std::shared_ptr<Node> nodePtr : getNodes()) {
+    // Compute dimensions of every node
+    std::set<std::shared_ptr<Node>> listNodes = getNodes();
+    do {
+        std::set<std::shared_ptr<Node>> nextList;
+        for (std::shared_ptr<Node> nodePtr : listNodes) {
             if (nodePtr->getOperator()->operatorType() == OperatorType::Tensor) {
-                if (!std::static_pointer_cast<OperatorTensor>(nodePtr->getOperator())->outputDimsForwarded()) {
-                    nextList.insert(nodePtr);
-                }
+              const auto op = std::static_pointer_cast<OperatorTensor>(nodePtr->getOperator());
+              // Recompute everytime, even if it was already computed in a
+              // previous call of forwardDims(), as the graph may have changed!
+              op->computeOutputDims();
+              if (!op->outputDimsForwarded()) {
+                  nextList.insert(nodePtr);
+              }
             }
         }
+
+        // Internal check to make sure we won't enter in an infinite loop!
+        if (nextList == listNodes) {
+            // We are stuck!
+            std::vector<std::string> nodesName;
+            std::transform(nextList.begin(), nextList.end(),
+                std::back_inserter(nodesName),
+                [](auto val){ return val->name() + " (" + val->type() + ")"; });
+            AIDGE_THROW_OR_ABORT(std::runtime_error, "Unable to forward dimensions (circular dependency and/or wrong dimensions?). Unable to compute output dims for nodes {}.", nodesName);
+        }
+
+        listNodes.swap(nextList);
     }
-    if (!nextList.empty()) {
-        _forwardDims(nextList);
-    }
+    while (!listNodes.empty());
 }
 
-void Aidge::GraphView::setBackend(const std::string &backend, DeviceIdx_t device) {
-    for (auto node : getNodes()) {
+void Aidge::GraphView::setBackend(const std::string &backend, const DeviceIdx_t device) const {
+    for (const auto& node : getNodes()) {
         node->getOperator()->setBackend(backend, device);
     }
 }
 
-void Aidge::GraphView::setDataType(const Aidge::DataType &datatype) {
-    for (auto node : getNodes()) {
+void Aidge::GraphView::setDataType(const Aidge::DataType &datatype) const {
+    for (const auto& node : getNodes()) {
         node->getOperator()->setDataType(datatype);
     }
 }
@@ -349,12 +483,14 @@ Aidge::GraphView::outputs() const {
       // Keep only the nodes connected at this output position that are outside the GraphView
       std::vector<std::pair<std::shared_ptr<Node>, Aidge::IOIndex_t>> outsideOutputPos;
       for (const auto& output : outputPos) {
-        if (mNodes.find(output.first) == mNodes.end()) {
+        if (output.first == nullptr || mNodes.find(output.first) == mNodes.end()) {
           outsideOutputPos.push_back(output);
         }
       }
 
-      outsideOutputs.push_back(outsideOutputPos);
+      if (outputPos.empty() || !outsideOutputPos.empty()) {
+        outsideOutputs.push_back(outsideOutputPos);
+      }
     }
   }
   return outsideOutputs;
@@ -362,16 +498,18 @@ Aidge::GraphView::outputs() const {
 
 std::vector<
     std::vector<std::pair<std::shared_ptr<Aidge::Node>, Aidge::IOIndex_t>>>
-Aidge::GraphView::outputs(std::string nodeName) const {
+Aidge::GraphView::outputs(const std::string& nodeName) const {
   return mNodeRegistry.at(nodeName)->outputs();
 }
 
 void Aidge::GraphView::setInputId(Aidge::IOIndex_t /*inID*/,
                                Aidge::IOIndex_t /*newNodeOutID*/) {
-  printf("Not implemented yet.\n");
+  AIDGE_THROW_OR_ABORT(std::runtime_error, "Not implemented yet.");
 }
 
 void Aidge::GraphView::add(std::shared_ptr<Node> node, bool includeLearnableParam) {
+  AIDGE_ASSERT(node != nullptr, "Trying to add non-existant node!");
+
   // first node to be added to the graph is the root node by default
   if (mRootNode == nullptr) {
     mRootNode = node;
@@ -402,6 +540,59 @@ void Aidge::GraphView::add(std::shared_ptr<Node> node, bool includeLearnablePara
   }
 }
 
+std::pair<std::vector<Aidge::NodePtr>, size_t> Aidge::GraphView::getRankedNodes() const {
+  std::set<NodePtr> nodesToRank(mNodes);
+  nodesToRank.erase(mRootNode);
+  std::vector<NodePtr> rankedNodes;
+  rankedNodes.push_back(mRootNode);
+
+  for (size_t curNodeIdx = 0; curNodeIdx < rankedNodes.size(); ++curNodeIdx) {
+    NodePtr curNode = rankedNodes[curNodeIdx];
+
+    for (auto childs : curNode->getOrderedChildren()) {
+      for (auto child : childs) {
+        if (child != nullptr && nodesToRank.find(child) != nodesToRank.end()) {
+          rankedNodes.push_back(child);
+          nodesToRank.erase(child);
+        }
+      }
+    }
+
+    for (auto parent : curNode->getParents()) {
+      if (parent != nullptr && nodesToRank.find(parent) != nodesToRank.end()) {
+        rankedNodes.push_back(parent);
+        nodesToRank.erase(parent);
+      }
+    }
+  }
+
+  const size_t orderUnicityLimit = rankedNodes.size();
+  if (!nodesToRank.empty()) {
+    rankedNodes.insert(rankedNodes.end(), nodesToRank.begin(), nodesToRank.end());
+  }
+
+  return std::make_pair(rankedNodes, orderUnicityLimit);
+}
+
+std::map<Aidge::NodePtr, std::string> Aidge::GraphView::getRankedNodesName(const std::string& format, bool markNonUnicity) const {
+  const auto rankedNodes = getRankedNodes();
+  std::map<NodePtr, std::string> rankedNodesName;
+  size_t rank = 0;
+  std::map<std::string, size_t> typeRank;
+  for (const auto& rankedNode : rankedNodes.first) {
+    std::map<std::string, size_t>::iterator it;
+    std::tie(it, std::ignore) = typeRank.insert(std::make_pair(rankedNode->type(), 0));
+
+    const auto name = (markNonUnicity && rank < rankedNodes.second)
+      ? fmt::format(format, rankedNode->name(), rankedNode->type(), rank, it->second)
+      : fmt::format(format, rankedNode->name(), rankedNode->type(), fmt::format("?{}", rank), fmt::format("?{}", it->second));
+    rankedNodesName.insert(std::make_pair(rankedNode, name));
+    ++it->second;
+    ++rank;
+  }
+  return rankedNodesName;
+}
+
 bool Aidge::GraphView::add(std::set<std::shared_ptr<Node>> otherNodes, bool includeLearnableParam) {
   if (otherNodes.empty()) {
     return true;
@@ -456,7 +647,7 @@ bool Aidge::GraphView::add(std::set<std::shared_ptr<Node>> otherNodes, bool incl
 
     for (auto childs : curNode->getOrderedChildren()) {
       for (auto child : childs) {
-        if (nodesToRank.find(child) != nodesToRank.end()) {
+        if (child != nullptr && nodesToRank.find(child) != nodesToRank.end()) {
           rankedNodes.push_back(child);
           nodesToRank.erase(child);
 
@@ -469,7 +660,7 @@ bool Aidge::GraphView::add(std::set<std::shared_ptr<Node>> otherNodes, bool incl
     }
 
     for (auto parent : curNode->getParents()) {
-      if (nodesToRank.find(parent) != nodesToRank.end()) {
+      if (parent != nullptr && nodesToRank.find(parent) != nodesToRank.end()) {
         rankedNodes.push_back(parent);
         nodesToRank.erase(parent);
 
@@ -508,11 +699,9 @@ bool Aidge::GraphView::add(std::pair<NodePtr, std::set<NodePtr>> nodes, bool inc
 }
 
 bool Aidge::GraphView::add(std::shared_ptr<GraphView> graph) {
-  if (mRootNode == nullptr) {
-    mRootNode = graph->getRootNode();
-  }
-
-  return add(graph->getNodes(), false);
+    // set the rootNode to the other graphView rootNode if no rootNode yet
+    mRootNode = mRootNode ? mRootNode : graph->rootNode();
+    return add(graph->getNodes(), false);
 }
 
 void Aidge::GraphView::addChild(std::shared_ptr<Node> toOtherNode,
@@ -569,10 +758,7 @@ std::set<std::shared_ptr<Aidge::Node>> Aidge::GraphView::getParents() const {
 
 std::vector<std::shared_ptr<Aidge::Node>> Aidge::GraphView::getParents(const std::string nodeName) const {
   std::map<std::string, std::shared_ptr<Node>>::const_iterator it = mNodeRegistry.find(nodeName);
-  if (it == mNodeRegistry.end()) {
-    printf("No such node a %s in %s graph.\n", nodeName.c_str(), name().c_str());
-    exit(-1);
-  }
+  AIDGE_ASSERT(it != mNodeRegistry.end(), "No node named {} in graph {}.", nodeName, name());
   return (it->second)->getParents();
 }
 
@@ -598,21 +784,15 @@ std::vector<std::vector<std::shared_ptr<Aidge::Node>>>
 Aidge::GraphView::getChildren(const std::string nodeName) const {
   std::map<std::string, std::shared_ptr<Node>>::const_iterator it =
       mNodeRegistry.find(nodeName);
-  if (it == mNodeRegistry.end()) {
-    printf("No such node a %s in %s graph.\n", nodeName.c_str(),
-           name().c_str());
-    exit(-1);
-  }
+  AIDGE_ASSERT(it != mNodeRegistry.end(), "No node named {} in graph {}.", nodeName, name());
   return (it->second)->getOrderedChildren();
 }
 
 std::set<std::shared_ptr<Aidge::Node>>
 Aidge::GraphView::getChildren(const std::shared_ptr<Node> otherNode) const {
   std::set<std::shared_ptr<Node>>::const_iterator it = mNodes.find(otherNode);
-  if (it == mNodes.end()) {
-    printf("No such node in graph.\n");
-    exit(-1);
-  }
+  AIDGE_ASSERT(it != mNodes.end(), "The node {} (of type {}) is not in graph {}.",
+    (otherNode) ? otherNode->name() : "#nullptr", (otherNode) ? otherNode->type() : "", name());
   return (*it)->getChildren();
 }
 
@@ -624,7 +804,7 @@ Aidge::GraphView::getNode(const std::string& nodeName) const {
   if (it != mNodeRegistry.cend()) {
     return it->second;
   } else {
-    printf("No Node named %s in the current GraphView.\n", nodeName.c_str());
+    Log::warn("No Node named {} in the current GraphView {}.", nodeName, name());
     return nullptr;
   }
 }
@@ -673,13 +853,13 @@ void Aidge::GraphView::remove(std::shared_ptr<Node> nodePtr, bool includeLearnab
 
 
 bool Aidge::GraphView::swap(Node & /*node*/, Node & /*otherNode*/) {
-  printf("Swap() not implementated yet. Return false.\n");
+  fmt::print("Swap() not implementated yet. Return false.\n");
   return false;
 }
 
-void Aidge::GraphView::link(std::string /*name1_inID*/,
-                           std::string /*name2_outID*/) {
-  printf("Not implemented yet.\n");
+void Aidge::GraphView::link(const std::string& /*name1_inID*/,
+                           const std::string& /*name2_outID*/) {
+  fmt::print("Not implemented yet.\n");
 }
 
 void Aidge::GraphView::insertParent(NodePtr childNode,
@@ -700,39 +880,54 @@ void Aidge::GraphView::insertParent(NodePtr childNode,
 }
 
 bool Aidge::GraphView::replace(const std::set<Aidge::NodePtr>& oldNodes, const std::set<Aidge::NodePtr>& newNodes) {
-    // TODO: handle case where an oldNodes parameter does not come from a Producer but another Node (not included in oldNodes)
-    // How to distinguish it from data input?
-    // TODO: Parameter Tensors could be identified with their dimensions
-    // TODO: Take GraphView as input parameters since new Nodes should be connected whatever.
-    // It also avoids specifying each producer since they are automatically included
-
     // (1) create GraphViews from both sets of Nodes
     auto oldG = std::make_shared<GraphView>("oldG");
     oldG->add(oldNodes, false);
     auto newG = std::make_shared<GraphView>("newG");
     newG->add(newNodes, false);
 
-    const auto oldOI = oldG->getOrderedInputs();
-    const auto oldOO = oldG->getOrderedOutputs();
-    const auto newOI = newG->getOrderedInputs();
-    const auto newOO = newG->getOrderedOutputs();
-    std::vector<std::pair<std::shared_ptr<Node>, IOIndex_t>> inputParents = std::vector<std::pair<std::shared_ptr<Node>, IOIndex_t>>(oldOI.size());
-    std::vector<std::pair<std::shared_ptr<Node>, IOIndex_t>> outputChildren = std::vector<std::pair<std::shared_ptr<Node>, IOIndex_t>>(oldOO.size());
+    return GraphView::replace(oldG, newG);
+}
 
-    // keep in memory every parent
-    for (std::size_t i = 0; i < oldOI.size(); ++i) {
-        auto inputParent = oldOI[i].first -> input(oldOI[i].second);
+bool Aidge::GraphView::replace(const std::shared_ptr<GraphView>& oldGraph, const std::shared_ptr<GraphView>& newGraph) {
+    // TODO: handle case where an oldNodes parameter does not come from a Producer but another Node (not included in oldNodes)
+    // How to distinguish it from data input?
+    // TODO: Parameter Tensors could be identified with their dimensions
+    // TODO: Take GraphView as input parameters since new Nodes should be connected whatever.
+    // It also avoids specifying each producer since they are automatically included
+    const std::set<NodePtr>&  oldNodes = oldGraph->getNodes();
+    const std::set<NodePtr>&  newNodes = newGraph->getNodes();
+
+    const std::vector<std::pair<NodePtr, IOIndex_t>> oldOIn =
+                                                     oldGraph->getOrderedInputs();
+    const std::vector<std::pair<NodePtr, IOIndex_t>> oldOOut =
+                                                     oldGraph->getOrderedOutputs();
+    const std::vector<std::pair<NodePtr, IOIndex_t>> newOIn =
+                                                     newGraph->getOrderedInputs();
+    const std::vector<std::pair<NodePtr, IOIndex_t>> newOOut =
+                                                     newGraph->getOrderedOutputs();
+
+    auto inputParents = std::vector<std::pair<std::shared_ptr<Node>, IOIndex_t>>(oldOIn.size());
+    auto outputChildren = std::vector<std::pair<std::shared_ptr<Node>, IOIndex_t>>(oldOOut.size());
+
+    // keep in memory every node related to the node to replace :
+    // Parent
+    for (std::size_t i = 0; i < oldOIn.size(); ++i) {
+        std::pair<NodePtr, IOIndex_t> inputParent =
+                  oldOIn[i].first -> input(oldOIn[i].second);
         inputParents[i]= inputParent;
         // inputParent.first -> addChild(newOI[i].first, inputParent.second, newOI[i].second);
     }
-    for (std::size_t i = 0; i < oldOO.size();) {
-        auto outputChildList = oldOO[i].first -> output(oldOO[i].second);
-        if (outputChildList.empty()) {
+    // Children
+    for (std::size_t i = 0; i < oldOOut.size();) {
+        std::vector<std::pair<std::shared_ptr<Aidge::Node>, Aidge::IOIndex_t>> outputChild =
+              oldOOut[i].first -> output(oldOOut[i].second);
+        if (outputChild.empty()) {
             outputChildren[i] = std::pair<std::shared_ptr<Node>, IOIndex_t>({nullptr, gk_IODefaultIndex});
             ++i;
         }
         else {
-            for (const auto& child : outputChildList) {
+            for (const auto& child : outputChild) {
                 if (oldNodes.find(child.first) == oldNodes.cend()) {
                     outputChildren[i] = child;
                     ++i;
@@ -745,37 +940,37 @@ bool Aidge::GraphView::replace(const std::set<Aidge::NodePtr>& oldNodes, const s
     // set of common GraphView for oldNodes' Nodes
     std::set<std::shared_ptr<GraphView>> commonGraphViews =  (*oldNodes.begin())->views();
     for (const auto& nodePtr : oldNodes) {
-        const auto nodeView = nodePtr->views();
+        const std::set<std::shared_ptr<GraphView>> nodeView = nodePtr->views();
         std::set<std::shared_ptr<GraphView>> intersection;
         std::set_intersection(commonGraphViews.begin(), commonGraphViews.end(),
                             nodeView.begin(), nodeView.end(),
                             std::inserter(intersection, intersection.begin()));
         commonGraphViews = intersection;
     }
-    commonGraphViews.erase(oldG);
-    commonGraphViews.erase(newG);
+    commonGraphViews.erase(oldGraph);
+    commonGraphViews.erase(newGraph);
 
-    if ((newNodes.size() > 0) && (oldOI.size() != newOI.size()) && (oldOO.size() != newOO.size())) {
+    if ((newNodes.size() > 0) && (oldOIn.size() != newOIn.size()) && (oldOOut.size() != newOOut.size())) {
         for (const auto& nodePtr : oldNodes) {
-            nodePtr->removeView(oldG);
+            nodePtr->removeView(oldGraph);
         }
         for (const auto& nodePtr : newNodes) {
-            nodePtr->removeView(newG);
+            nodePtr->removeView(newGraph);
         }
         return false;
     }
 
-    if ((oldOI.size() == newOI.size()) &&
-        (oldOO.size() == newOO.size())) {
+    if ((oldOIn.size() == newOIn.size()) &&
+        (oldOOut.size() == newOOut.size())) {
         // Case 1
-        for (std::size_t i = 0; i < oldOI.size(); ++i) {
+        for (std::size_t i = 0; i < oldOIn.size(); ++i) {
             if (inputParents[i].first) {
-                inputParents[i].first -> addChild(newOI[i].first, inputParents[i].second, newOI[i].second);
+                inputParents[i].first -> addChild(newOIn[i].first, inputParents[i].second, newOIn[i].second);
             }
         }
-        for (std::size_t o = 0; o < oldOO.size(); ++o) {
+        for (std::size_t o = 0; o < oldOOut.size(); ++o) {
             if (outputChildren[o].first) {
-                newOO[o].first -> addChild(outputChildren[o].first, newOO[o].second, outputChildren[o].second);
+                newOOut[o].first -> addChild(outputChildren[o].first, newOOut[o].second, outputChildren[o].second);
             }
         }
     }
@@ -784,52 +979,53 @@ bool Aidge::GraphView::replace(const std::set<Aidge::NodePtr>& oldNodes, const s
         // get the number of Children for oldg->outputNodes()
         if (newNodes.size() == 0) {
             // Case 3
-            if (oldOI.size() == oldOO.size()) {
-                for (std::size_t i = 0; i < oldOI.size(); ++i) {
-                    if (inputParents[i].first)
-                        inputParents[i].first -> addChild(outputChildren[i].first, inputParents[i].second, outputChildren[i].second);
+            if (oldOIn.size() == oldOOut.size()) {
+                for (std::size_t i = 0; i < oldOIn.size(); ++i) {
+                    if (inputParents[i].first) {
+                      inputParents[i].first -> addChild(outputChildren[i].first, inputParents[i].second, outputChildren[i].second);
+                    }
                 }
             }
-            else if ((oldOI.size() == 1) && (inputParents[0].first)) {
-                for (std::size_t i = 0; i < oldOI.size(); ++i) {
+            else if ((oldOIn.size() == 1) && (inputParents[0].first)) {
+                for (std::size_t i = 0; i < oldOIn.size(); ++i) {
                     inputParents[0].first -> addChild(outputChildren[i].first, inputParents[0].second, outputChildren[i].second);
                 }
             }
         }
         else if ( // for tiling-like cases. The number of inputNodes changes but not outputNodes
-            ((oldOI.size() == 1) || (newOI.size() == 1)) && // (oldOI.size() == newOI.size()) already handled in Case 1
-            ((oldOO.size() == newOO.size()))
+            ((oldOIn.size() == 1) || (newOIn.size() == 1)) && // (oldOIn.size() == newOI.size()) already handled in Case 1
+            ((oldOOut.size() == newOOut.size()))
         ) {
             // Case 2
-            if ((oldOI.size() == 1) && (inputParents[0].first)) {
-                for (std::size_t i = 0; i < newOI.size(); ++i) {
-                    inputParents[0].first -> addChild(newOI[i].first, inputParents[0].second, newOI[i].second);
+            if ((oldOIn.size() == 1) && (inputParents[0].first)) {
+                for (std::size_t i = 0; i < newOIn.size(); ++i) {
+                    inputParents[0].first -> addChild(newOIn[i].first, inputParents[0].second, newOIn[i].second);
                 }
             } else {
-                for (std::size_t i = 0; i < oldOI.size(); ++i) {
+                for (std::size_t i = 0; i < oldOIn.size(); ++i) {
                     if (inputParents[i].first) {
-                        inputParents[i].first -> addChild(newOI[0].first, inputParents[i].second, newOI[0].second);
+                        inputParents[i].first -> addChild(newOIn[0].first, inputParents[i].second, newOIn[0].second);
                     }
                 }
             }
-            for (std::size_t o = 0; o < oldOO.size(); ++o) {
+            for (std::size_t o = 0; o < oldOOut.size(); ++o) {
                 if (outputChildren[o].first) {
-                    newOO[o].first -> addChild(outputChildren[o].first, newOO[o].second, outputChildren[o].second);
+                    newOOut[o].first -> addChild(outputChildren[o].first, newOOut[o].second, outputChildren[o].second);
                 }
             }
         }
         else {
             for (const auto& nodePtr : oldNodes) {
-                nodePtr->removeView(oldG);
+                nodePtr->removeView(oldGraph);
             }
             for (const auto& nodePtr : newNodes) {
-                nodePtr->removeView(newG);
+                nodePtr->removeView(newGraph);
             }
             return false;
         }
     }
 
-    auto oldGOutputs = oldG->outputNodes();
+    auto oldGOutputs = oldGraph->outputNodes();
     for (const auto& nodePtr : oldNodes) {
         bool removeFromGraphs = true;
         if (std::find(oldGOutputs.cbegin(), oldGOutputs.cend(), nodePtr) == oldGOutputs.cend()) {
@@ -855,10 +1051,10 @@ bool Aidge::GraphView::replace(const std::set<Aidge::NodePtr>& oldNodes, const s
         }
     }
     for (const auto& nodePtr : oldNodes) {
-        nodePtr -> removeView(oldG);
+        nodePtr -> removeView(oldGraph);
     }
     for (const auto& nodePtr : newNodes) {
-        nodePtr -> removeView(newG);
+        nodePtr -> removeView(newGraph);
     }
     return true;
 }
@@ -1059,6 +1255,17 @@ void Aidge::GraphView::updateInputsOutputsDelete(std::shared_ptr<Node> deletedNo
       }
     }
   }
+
+  if (deletedNode == mRootNode) {
+    const std::pair<std::vector<NodePtr>, size_t> ranked_nodes = getRankedNodes();
+    if(ranked_nodes.second== 0 || ranked_nodes.first.size() <= 1)
+    {
+      mRootNode = nullptr;
+    } else {
+      // The new root node will be the second node in the order of ranked nodes
+      setRootNode(*std::next(ranked_nodes.first.cbegin(),1));
+    }
+  }
 }
 
 
@@ -1198,3 +1405,31 @@ std::shared_ptr<Aidge::GraphView> Aidge::GraphView::cloneCallback(NodePtr(*clone
 
   return newGraph;
 }
+
+std::shared_ptr<Aidge::GraphView> Aidge::getConnectedGraphView(std::shared_ptr<Node> node) {
+  std::vector<NodePtr> foundNodes;
+  foundNodes.push_back(node);
+
+  for (size_t curNodeIdx = 0; curNodeIdx < foundNodes.size(); ++curNodeIdx) {
+    NodePtr curNode = foundNodes[curNodeIdx];
+
+    for (auto childs : curNode->getOrderedChildren()) {
+      for (auto child : childs) {
+        if (child != nullptr && std::find(foundNodes.begin(), foundNodes.end(), child) == foundNodes.end()) {
+          foundNodes.push_back(child);
+        }
+      }
+    }
+
+    for (auto parent : curNode->getParents()) {
+      if (parent != nullptr && std::find(foundNodes.begin(), foundNodes.end(), parent) == foundNodes.end()) {
+        foundNodes.push_back(parent);
+      }
+    }
+  }
+
+  auto graph = std::make_shared<GraphView>();
+  graph->add(node);
+  graph->add({foundNodes.cbegin(), foundNodes.cend()});
+  return graph;
+}
diff --git a/src/graph/Node.cpp b/src/graph/Node.cpp
index 6f0cc55159b1cc72b87bb34230376eb140b7ab8a..149691f796d1d84212e9d7842a28e4cb79469e6a 100644
--- a/src/graph/Node.cpp
+++ b/src/graph/Node.cpp
@@ -169,9 +169,11 @@ Aidge::IOIndex_t Aidge::Node::nbValidOutputs() const {
 }
 
 void Aidge::Node::setInputId(const IOIndex_t inId, const IOIndex_t newNodeoutId) {
-    assert(inId != gk_IODefaultIndex && (inId < nbInputs()) && "Must be a valid index");
+    AIDGE_ASSERT(inId != gk_IODefaultIndex && inId < nbInputs(),
+        "Input index ({}) is out of bound ({}) for node {} (of type {})",
+        inId, nbInputs(), name(), type());
     if (mIdOutParents[inId] != gk_IODefaultIndex) {
-        std::printf("Warning: filling a Tensor already attributed\n");
+        Log::notice("Notice: filling a Tensor already attributed");
         auto originalParent = input(inId);
         // remove original parent reference to child
         // find the output ID for original Parent
@@ -187,10 +189,14 @@ void Aidge::Node::setInputId(const IOIndex_t inId, const IOIndex_t newNodeoutId)
 
 void Aidge::Node::addChildOp(std::shared_ptr<Node> otherNode, const IOIndex_t outId,
                              const IOIndex_t otherInId) {
-    assert((otherInId < otherNode->nbInputs()) && "Input index out of bound.");
-    assert((outId < nbOutputs()) && "Output index out of bound.");
+    AIDGE_ASSERT(otherInId < otherNode->nbInputs(),
+        "Input index (#{}) of the node {} (of type {}) is out of bound (it has {} inputs), when trying to add it as a child of node {} (of type {})",
+        otherInId, otherNode->name(), otherNode->type(), otherNode->nbInputs(), name(), type());
+    AIDGE_ASSERT(outId < nbOutputs(),
+        "Output index (#{}) of the node {} (of type {}) is out of bound (it has {} outputs), when trying to add the child node {} (of type {})",
+        outId, name(), type(), nbOutputs(), otherNode->name(), otherNode->type());
     if (otherNode->input(otherInId).second != gk_IODefaultIndex) {
-        std::printf("Warning, the %d-th Parent of the child node already existed.\n", otherInId);
+        Log::notice("Notice: the {}-th Parent of the child node {} (of type {}) already existed", otherInId, otherNode->name(), otherNode->type());
     }
     // manage tensors and potential previous parent
     otherNode->setInputId(otherInId, outId);
@@ -203,18 +209,11 @@ void Aidge::Node::addChildOp(std::shared_ptr<Node> otherNode, const IOIndex_t ou
 
 void Aidge::Node::addChildView(std::shared_ptr<GraphView> otherGraph, const IOIndex_t outId,
                                std::pair<std::shared_ptr<Node>, IOIndex_t> otherInId) {
-    assert((otherInId.second < otherInId.first->nbInputs()) &&
-           "Other graph input index out of bound.");
-    assert((outId < nbOutputs()) && "Output index out of bound.");
-    std::set<std::shared_ptr<Node>> inNodes = otherGraph->inputNodes();
-    if (inNodes.size() == std::size_t(0)) {  // no input Node
-        printf("Cannot add GraphView to the Node. No input node detected.\n");
-    } else  // inNodes.size() >= 1
-    {
-        assert((inNodes.find(otherInId.first) !=
-                inNodes.end()));  // assert it really is an input node
-        addChildOp(otherInId.first, outId, otherInId.second);
-    }
+    const auto inNodes = otherGraph->inputNodes();
+    AIDGE_ASSERT(otherInId.first != nullptr && inNodes.find(otherInId.first) != inNodes.end(),
+        "Node {} (of type {}) is not a valid input node of GraphView {}, when trying to add it as a child of node {} (of type {})",
+        (otherInId.first) ? otherInId.first->name() : "#nullptr", (otherInId.first) ? otherInId.first->type() : "", otherGraph->name(), name(), type());
+    addChildOp(otherInId.first, outId, otherInId.second);
 }
 
 void Aidge::Node::addChild(std::shared_ptr<Node> otherNode, const IOIndex_t outId,
@@ -229,9 +228,9 @@ void Aidge::Node::addChild(std::shared_ptr<Node> otherNode, const IOIndex_t outI
 void Aidge::Node::addChild(std::shared_ptr<GraphView> otherView, const IOIndex_t outId,
                            std::pair<std::shared_ptr<Node>, IOIndex_t> otherInId) {
     if (!otherInId.first) {
-        assert((otherView->inputNodes().size() == 1U) &&
-               "Specify an input Node for the GraphView. More or less than one "
-               "Node is not explicit.");
+        AIDGE_ASSERT(otherView->inputNodes().size() == 1U,
+            "Input node of GraphView {} need to be specified, because it has more than one input ({} inputs), when trying to add it as a child of node {} (of type {})",
+            otherView->name(), otherView->inputNodes().size(), name(), type());
         otherInId.first = *(otherView->inputNodes().begin());
     }
     otherInId.second = (otherInId.second != gk_IODefaultIndex)
@@ -242,23 +241,29 @@ void Aidge::Node::addChild(std::shared_ptr<GraphView> otherView, const IOIndex_t
 
 void Aidge::Node::addParent(const std::shared_ptr<Node> other_node, const IOIndex_t inId) {
     if (getParent(inId) != nullptr) {
-        printf("Warning, you're replacing a Parent.\n");
+        Log::notice("Notice: you are replacing an existing parent for node {} (of type {})", name(), type());
     }
-    assert((inId != gk_IODefaultIndex) && (inId < nbInputs()) && "Input index out of bound.");
+    AIDGE_ASSERT(inId != gk_IODefaultIndex && inId < nbInputs(),
+        "Input index ({}) is out of bound ({}) for node {} (of type {})",
+        inId, nbInputs(), name(), type());
     mParents[inId] = other_node;
 }
 
 std::vector<std::shared_ptr<Aidge::Node>> Aidge::Node::getParents() const { return mParents; }
 
 std::shared_ptr<Aidge::Node> Aidge::Node::popParent(const IOIndex_t inId) {
-    assert((inId != gk_IODefaultIndex) && (inId < nbInputs()) && "Input index out of bound.");
+    AIDGE_ASSERT(inId != gk_IODefaultIndex && inId < nbInputs(),
+        "Input index ({}) is out of bound ({}) for node {} (of type {})",
+        inId, nbInputs(), name(), type());
     std::shared_ptr<Node> val = mParents[inId];
     removeParent(inId);
     return val;
 }
 
 bool Aidge::Node::removeParent(const IOIndex_t inId) {
-    assert((inId != gk_IODefaultIndex) && (inId < nbInputs()) && "Parent index out of bound.");
+    AIDGE_ASSERT(inId != gk_IODefaultIndex && inId < nbInputs(),
+        "Input index ({}) is out of bound ({}) for node {} (of type {})",
+        inId, nbInputs(), name(), type());
     if (mParents[inId]) {
         mParents[inId] = nullptr;
         mIdOutParents[inId] = gk_IODefaultIndex;
@@ -278,7 +283,7 @@ std::set<std::shared_ptr<Aidge::Node>> Aidge::Node::getChildren() const {
 }
 
 std::vector<std::vector<std::shared_ptr<Aidge::Node>>> Aidge::Node::getOrderedChildren() const {
-    std::vector<std::vector<std::shared_ptr<Node>>> children =
+    auto children =
             std::vector<std::vector<std::shared_ptr<Node>>>(mChildren.size());
     for (std::size_t outId = 0; outId < mChildren.size(); ++outId) {
         children[outId] = getChildren(outId);
@@ -288,8 +293,7 @@ std::vector<std::vector<std::shared_ptr<Aidge::Node>>> Aidge::Node::getOrderedCh
 
 std::vector<std::shared_ptr<Aidge::Node>> Aidge::Node::getChildren(const IOIndex_t outId) const {
     assert((outId < nbOutputs()) && "Output index out of bound.");
-    std::vector<std::shared_ptr<Node>> children =
-            std::vector<std::shared_ptr<Node>>(mChildren[outId].size());
+    std::vector<std::shared_ptr<Node>> children;
     for (std::size_t i = 0; i < mChildren[outId].size(); ++i) {
         children.push_back(mChildren[outId][i].lock());
     }
@@ -386,6 +390,26 @@ std::set<Aidge::NodePtr> Aidge::Node::getNodeDelta(int delta, std::set<Aidge::No
 
     return out;
 }
+
+// namespace Aidge {
+// std::ostream& operator << (std::ostream& os, Aidge::Node& n) {
+//     using namespace std;
+//     os << "Node :\tName :\t\"" << n.name() << "\"\tType : \"" << n.getOperator()->type()<< "\"\tIN/OUTputs : "<< n.nbInputs() <<"/"<< n.nbOutputs() <<endl;
+//     os << "\tParents :\t" ;
+//     for (const auto & p : n.getParents())
+//     {
+//         os << "\"" <<p->name() << "\"\t";
+//     }
+//     os << endl;
+//     os << "\tChildren :\t" ;
+//     for (const auto & c : n.getChildren())
+//     {
+//         os << "\"" << c->name() << "\"\t";
+//     }
+//     os << endl;
+//     return os;
+// }
+// }
 /////////////////////////////////////////////////////////////////////////////////////////////
 // private
 
diff --git a/src/graphRegex/GraphRegex.cpp b/src/graphRegex/GraphRegex.cpp
index 00a031e3fa9b03ff1870446b9ae58e8d3eb65bf7..ca15ff8dec5ff5ebd4ea69141c6e286849162bb5 100644
--- a/src/graphRegex/GraphRegex.cpp
+++ b/src/graphRegex/GraphRegex.cpp
@@ -117,6 +117,8 @@ std::set<std::shared_ptr<MatchSolution>> GraphRegex::match(std::shared_ptr<Graph
             std::vector<std::shared_ptr<MatchSolution>> solution = fsm->test(combination);
             solutions.insert(solutions.end(), solution.begin(), solution.end());
         }
+
+
     }
     return _findLargestCompatibleSet(solutions);
 }
@@ -142,7 +144,10 @@ void GraphRegex::setNodeKey(const std::string key,std::function<bool(NodePtr)> f
         throw std::runtime_error(key + " is define");
     }
     mAllLambda[key] = f;
+    
     _majConditionalInterpreterLambda();
+    //we add the lambda as key by default 
+    setNodeKey(key, key + "($)==true");
 }
 
 void GraphRegex::_majConditionalInterpreterLambda(){
diff --git a/src/nodeTester/ConditionalLexer.cpp b/src/nodeTester/ConditionalLexer.cpp
index 9379bd8409f8f7ec4bae3e0122f88de79718e9dd..e70772fc1a5d6136fb56f5981d73bf6cb0622991 100644
--- a/src/nodeTester/ConditionalLexer.cpp
+++ b/src/nodeTester/ConditionalLexer.cpp
@@ -120,7 +120,7 @@ std::shared_ptr<ParsingToken<ConditionalTokenTypes>> ConditionalLexer::getNextTo
             }
 
 
-            if (std::regex_match(currentChars,std::regex("(true|false)"))){
+            if (std::regex_match(currentChars,std::regex("(true|false|True|False)"))){
                 return std::make_shared<ParsingToken<ConditionalTokenTypes>>(ConditionalTokenTypes::BOOL,currentChars);
 
             } else if (isLambda){
diff --git a/src/operator/Add.cpp b/src/operator/Add.cpp
index 4e638fd86da487565a89760925e45339213fa8f9..85bc4b7aef53e8064a8f31815a42689013880812 100644
--- a/src/operator/Add.cpp
+++ b/src/operator/Add.cpp
@@ -9,8 +9,70 @@
  *
  ********************************************************************************/
 
+#include <cstddef>    // std::size_t
+#include <stdexcept>  // std::runtime_error
 #include <string>
+#include <vector>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Add.hpp"
+#include "aidge/utils/Types.h"
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Registrar.hpp"
 
-const std::string Aidge::Add_Op::Type = "Add";
\ No newline at end of file
+const std::string Aidge::Add_Op::Type = "Add";
+
+Aidge::Add_Op::Add_Op(const Add_Op& op)
+    : OperatorTensor(op)
+{
+    if (op.mImpl) {
+        SET_IMPL_MACRO(Add_Op, *this, op.backend());
+    } else {
+        mImpl = nullptr;
+    }
+}
+
+void Aidge::Add_Op::computeOutputDims() {
+    // check inputs have been associated
+    bool associated = (nbInputs() > 0); // do not compute anything if no input
+    for (IOIndex_t i = 0; i < nbInputs(); ++i) {
+        if (!getInput(i)) {
+            AIDGE_THROW_OR_ABORT(std::runtime_error, "Every input should be associated with a Tensor");
+        }
+        associated &= !(getInput(i)->empty());
+    }
+    if (associated) {
+        std::vector<std::vector<std::size_t>> inputsDims(nbInputs());
+        for (std::size_t i = 0; i < nbInputs(); i++) {
+            inputsDims[i] = getInput(i)->dims();
+        }
+
+        std::size_t outNbDims = 1;
+        for(std::size_t i = 0; i < nbInputs(); ++i) {
+            outNbDims = (inputsDims[i].size() > outNbDims) ? inputsDims[i].size() : outNbDims;
+        }
+
+        std::vector<std::size_t> outDims(outNbDims, 1);
+
+        for (auto it = outDims.rbegin(); it != outDims.rend(); ++it) {
+            for (std::size_t i = 0; i < nbInputs(); ++i) {
+                if(!inputsDims[i].empty()) {
+                    const std::size_t dim = inputsDims[i].back();
+                    inputsDims[i].pop_back();
+                    if (*it == 1) {
+                        *it = dim;
+                    }
+                    else if ((dim != *it) && (dim != 1)) {
+                        AIDGE_THROW_OR_ABORT(std::runtime_error, "Unsopported Tensor shape for Add operation");
+                    }
+                }
+            }
+        }
+        mOutputs[0]->resize(outDims);
+    }
+}
+
+void Aidge::Add_Op::setBackend(const std::string& name, DeviceIdx_t device) {
+    SET_IMPL_MACRO(Add_Op, *this, name);
+    mOutputs[0]->setBackend(name, device);
+}
\ No newline at end of file
diff --git a/src/operator/AvgPooling.cpp b/src/operator/AvgPooling.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..acb097668bce0ff6f335f577faed503e086db79f
--- /dev/null
+++ b/src/operator/AvgPooling.cpp
@@ -0,0 +1,112 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include "aidge/operator/AvgPooling.hpp"
+
+#include <cmath>      // std::floor
+#include <cstddef>    // std::size_t
+#include <stdexcept>  // std::runtime_error
+#include <string>
+#include <utility>    // std::pair
+#include <vector>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
+
+template <Aidge::DimIdx_t DIM>
+const std::string Aidge::AvgPooling_Op<DIM>::Type = "AvgPooling";
+
+template <Aidge::DimIdx_t DIM>
+Aidge::AvgPooling_Op<DIM>::AvgPooling_Op(const AvgPooling_Op<DIM>& op): OperatorTensor(op), Attributes_(op) {
+    if (op.mImpl) {
+        SET_IMPL_MACRO(AvgPooling_Op<DIM>, *this, op.backend());
+    } else {
+        mImpl = nullptr;
+    }
+}
+
+template <Aidge::DimIdx_t DIM>
+void Aidge::AvgPooling_Op<DIM>::computeOutputDims() {
+    // check inputs have been associated
+    if (!getInput(0)) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "{}: input #0 should be associated with a Tensor", type());
+    }
+    if (!(getInput(0)->empty())) {
+        std::array<DimSize_t, DIM + 2> outputDims;
+        const std::array<DimSize_t, DIM + 2> inputDims(getInput(0)->template dims<DIM+2>());
+        outputDims[0] = inputDims[0];
+        outputDims[1] = inputDims[1];
+
+        for (std::size_t dim = 0; dim < this->template getAttr<AvgPoolingAttr::KernelDims>().size() ; ++dim) {
+            outputDims[dim+2] = 1 + static_cast<DimSize_t>(
+                                        std::floor(static_cast<float>(inputDims[dim+2] -
+                                                                this->template getAttr<AvgPoolingAttr::KernelDims>()[dim]) /
+                                        static_cast<float>(this->template getAttr<AvgPoolingAttr::StrideDims>()[dim])));
+        }
+        getOutput(0)->resize(outputDims);
+    }
+}
+
+
+template <Aidge::DimIdx_t DIM>
+std::vector<std::pair<std::vector<Aidge::DimSize_t>, std::vector<Aidge::DimSize_t>>>
+Aidge::AvgPooling_Op<DIM>::computeReceptiveField(const std::vector<Aidge::DimSize_t>& firstEltDims,
+                        const std::vector<Aidge::DimSize_t>& outputDims,
+                        const Aidge::IOIndex_t outputIdx) const {
+    if (outputIdx != 0) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "Conv_Op Operator has got only one output Tensor.");
+    }
+    if (firstEltDims.size() != outputDims.size()) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "outputDims and firstEltDims should have the size of the output Tensor dimensions.");
+    }
+    if ((outputDims.size() == (DIM+2)) && outputDimsForwarded()) {
+        // Offset
+        std::vector<DimSize_t> inputIdxDims = firstEltDims;
+
+        for (DimIdx_t i = 0; i < (DIM+2); ++i) {
+            if (((outputDims[i] + firstEltDims[i]) > mOutputs[0]->template dims<DIM+2>()[i]) || (outputDims[i] == 0)) {
+                AIDGE_THROW_OR_ABORT(std::runtime_error, "Given outputDim out of range for dimension {} ({} + {})", static_cast<std::size_t>(i), firstEltDims[i], outputDims[i]);
+            }
+        }
+
+        // padding is not a parameter of Conv_Op. It is handled in Pad_Op Operator
+        // Width
+        std::vector<DimSize_t> inputDims;
+        inputDims.push_back(outputDims[0]); // same batch value
+        inputDims.push_back(outputDims[1]); // same channel value
+
+        for (DimIdx_t i = 0; i < DIM; ++i) {
+            inputDims.push_back((outputDims[2+static_cast<std::size_t>(i)] - 1)
+                        * this->template getAttr<AvgPoolingAttr::StrideDims>()[static_cast<std::size_t>(i)]
+                        + 1
+                        + (this->template getAttr<AvgPoolingAttr::KernelDims>()[static_cast<std::size_t>(i)] - 1));
+            inputIdxDims[2+i] *= this->template getAttr<AvgPoolingAttr::StrideDims>()[static_cast<std::size_t>(i)];
+        }
+        std::vector<std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>> res;
+        res.push_back(std::pair<std::vector<Aidge::DimSize_t>, std::vector<DimSize_t>>(inputIdxDims, inputDims));
+        return res;
+    }
+    AIDGE_THROW_OR_ABORT(std::runtime_error, "Given outputDim out of range or output dim not forwarded yet.");
+}
+
+
+template <Aidge::DimIdx_t DIM>
+void Aidge::AvgPooling_Op<DIM>::setBackend(const std::string &name, Aidge::DeviceIdx_t device) {
+    SET_IMPL_MACRO(AvgPooling_Op<DIM>, *this, name);
+    mOutputs[0]->setBackend(name, device);
+}
+
+template class Aidge::AvgPooling_Op<1>;
+template class Aidge::AvgPooling_Op<2>;
+template class Aidge::AvgPooling_Op<3>;
+template class Aidge::AvgPooling_Op<4>;
\ No newline at end of file
diff --git a/src/operator/BatchNorm.cpp b/src/operator/BatchNorm.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b14f0238809b9ec9b6b186d093ecf3b1554865cb
--- /dev/null
+++ b/src/operator/BatchNorm.cpp
@@ -0,0 +1,90 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include "aidge/operator/BatchNorm.hpp"
+
+#include <cstddef>    // std::size_t
+#include <stdexcept>  // std::runtime_error
+#include <string>
+#include <utility>    // std::pair
+#include <vector>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/operator/Producer.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
+
+template <Aidge::DimIdx_t DIM>
+const std::string Aidge::BatchNorm_Op<DIM>::Type = "BatchNorm";
+
+template <Aidge::DimIdx_t DIM>
+Aidge::BatchNorm_Op<DIM>::BatchNorm_Op(const BatchNorm_Op<DIM>& op): OperatorTensor(op), Attributes_(op) {
+    if (op.mImpl) {
+        SET_IMPL_MACRO(BatchNorm_Op<DIM>, *this, op.backend());
+    } else {
+        mImpl = nullptr;
+    }
+}
+
+template <Aidge::DimIdx_t DIM>
+void Aidge::BatchNorm_Op<DIM>::computeOutputDims() {
+    // check inputs have been associated
+    bool associated = true;
+    for (IOIndex_t i = 0; i < nbInputs(); ++i) {
+        associated &= !(getInput(i)->empty());
+    }
+    if (associated) {
+        const DimSize_t nbFeatures =  getInput(0)->dims()[1];
+        for (std::size_t i = nbData(); i < nbInputs(); ++i) {
+            if(getInput(i)->size() != nbFeatures) {
+                // /!\ Input size should be handled BEFORE calling this function
+                // This should raise an error
+                getInput(i)->resize({getInput(0)->dims()[1]});
+            }
+        }
+        mOutputs[0]->resize(getInput(0)->dims());
+    }
+}
+
+template <Aidge::DimIdx_t DIM>
+void Aidge::BatchNorm_Op<DIM>::setBackend(const std::string &name, Aidge::DeviceIdx_t device) {
+    SET_IMPL_MACRO(BatchNorm_Op<DIM>, *this, name);
+    mOutputs[0]->setBackend(name, device);
+
+    // By default, automatically set backend for scale, shift, mean and variance
+    getInput(1)->setBackend(name, device);
+    getInput(2)->setBackend(name, device);
+    getInput(3)->setBackend(name, device);
+    getInput(4)->setBackend(name, device);
+}
+
+template class Aidge::BatchNorm_Op<2>;
+template class Aidge::BatchNorm_Op<3>;
+template class Aidge::BatchNorm_Op<4>;
+
+template <Aidge::DimSize_t DIM>
+inline std::shared_ptr<Aidge::Node> Aidge::BatchNorm(const DimSize_t nbFeatures,
+                                       const float epsilon,
+                                       const float momentum,
+                                       const std::string& name) {
+    static_assert(DIM<=MaxDim,"Too many kernel dimensions required by BatchNorm, not supported");
+    auto batchNorm = std::make_shared<Node>(std::make_shared<BatchNorm_Op<static_cast<DimIdx_t>(DIM)>>(epsilon, momentum), name);
+    addProducer(batchNorm, 1, {nbFeatures}, "scale");
+    addProducer(batchNorm, 2, {nbFeatures}, "shift");
+    addProducer(batchNorm, 3, {nbFeatures}, "batch_mean");
+    addProducer(batchNorm, 4, {nbFeatures}, "batch_variance");
+    return batchNorm;
+}
+
+template std::shared_ptr<Aidge::Node> Aidge::BatchNorm<2>(const DimSize_t, const float, const float, const std::string&);
+template std::shared_ptr<Aidge::Node> Aidge::BatchNorm<3>(const DimSize_t, const float, const float, const std::string&);
+template std::shared_ptr<Aidge::Node> Aidge::BatchNorm<4>(const DimSize_t, const float, const float, const std::string&);
\ No newline at end of file
diff --git a/src/operator/Cast.cpp b/src/operator/Cast.cpp
index f09d8eb83c6a6dae6416ffebcc01b22fb479a862..4f1ac55898b11668ba1c2f5299f8e1ca1d4e5df1 100644
--- a/src/operator/Cast.cpp
+++ b/src/operator/Cast.cpp
@@ -9,9 +9,17 @@
  *
  ********************************************************************************/
 
-#include "aidge/backend/OperatorImpl.hpp"
 #include "aidge/operator/Cast.hpp"
 
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
+
 const std::string Aidge::Cast_Op::Type = "Cast";
 
 void Aidge::Cast_Op::forward() {
@@ -24,3 +32,10 @@ void Aidge::Cast_Op::forward() {
 
     runHooks();
 }
+
+void Aidge::Cast_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    if (Registrar<Cast_Op>::exists({name})) {
+        SET_IMPL_MACRO(Cast_Op, *this, name);
+    }
+    mOutputs[0]->setBackend(name, device);
+}
diff --git a/src/operator/Concat.cpp b/src/operator/Concat.cpp
index eafcd126480df6da2c0127bdbb896d3ce98d0e0a..7df5b6dbf6122da44aed280da0d717232ba42fef 100644
--- a/src/operator/Concat.cpp
+++ b/src/operator/Concat.cpp
@@ -9,8 +9,49 @@
  *
  ********************************************************************************/
 
+#include "aidge/operator/Concat.hpp"
+
 #include <string>
+#include <vector>
 
-#include "aidge/operator/Concat.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/StaticAttributes.hpp"
+#include "aidge/utils/Types.h"
+
+const std::string Aidge::Concat_Op::Type = "Concat";
+
+void Aidge::Concat_Op::computeOutputDims() {
+    // Every input is non-empty with the same number of dimensions
+    bool associated = (getInput(0) != nullptr);
+    associated &= !(getInput(0)->empty()) && (getAttr<ConcatAttr::Axis>() < getInput(0)->nbDims()); // do not compute anything if no input
+    auto outputDims =  getInput(0)->dims();
+    const auto firstInputNbDims = getInput(0) -> nbDims();
+    for (IOIndex_t i = 1; i < nbInputs(); ++i) {
+        if (!getInput(i)) {
+            AIDGE_THROW_OR_ABORT(std::runtime_error, "{}: input #{} should be associated with a Tensor", type(), i);
+        }
+
+        if (getInput(i)->nbDims() == firstInputNbDims) {
+            for (DimSize_t dim = 0; dim < firstInputNbDims; ++dim) {
+                if (dim == getAttr<ConcatAttr::Axis>()) {
+                    outputDims[dim] += getInput(i)->dims()[dim];
+                }
+                else {
+                    associated &= (getInput(i)->dims()[dim] == outputDims[dim]);
+                }
+            }
+        }
+        else {
+            associated = false;
+            break;
+        }
+    }
+    if (associated) {
+        getOutput(0)->resize(outputDims);
+    }
+}
 
-const std::string Aidge::Concat_Op::Type = "Concat";
\ No newline at end of file
+void Aidge::Concat_Op::setBackend(const std::string& name, DeviceIdx_t device) {
+    SET_IMPL_MACRO(Concat_Op, *this, name);
+    mOutputs[0]->setBackend(name, device);
+}
diff --git a/src/operator/Div.cpp b/src/operator/Div.cpp
index 85db3ac6ef66c837c86dbece288185deaca88ba6..5ffe5f08dbcbfe42c406846990c432a7fbd325e0 100644
--- a/src/operator/Div.cpp
+++ b/src/operator/Div.cpp
@@ -9,12 +9,12 @@
  *
  ********************************************************************************/
 
-#include <cassert>
-#include <cstddef>
+#include <cstddef>    // std::size_t
+#include <stdexcept>  // std::runtime_error
 #include <string>
 #include <vector>
-#include <utility>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/backend/OperatorImpl.hpp"
 #include "aidge/operator/Div.hpp"
 #include "aidge/utils/Types.h"
@@ -28,11 +28,33 @@ void Aidge::Div_Op::computeOutputDims() {
         AIDGE_THROW_OR_ABORT(std::runtime_error, "At least one input was not connected");
     }
 
-    if ((!getInput(0)->empty()) &&
-        ((getInput(1)->size() == 1) || // div by a single value
-        (getInput(1)->size() == getInput(0)->size()) || // div elem-wise
-        (getInput(1)->nbDims() == 1 && getInput(1)->size() == getInput(0)->dims()[getInput(0)->nbDims()-1]))) // div by a Tensor with one dimension of output size
-    {
-        mOutputs[0]->resize(getInput(0)->dims());
+    if (!getInput(0)->empty() && !getInput(1)->empty()) {
+
+        const std::vector<std::size_t>& inputsDims0 = getInput(0)->dims();
+        const std::vector<std::size_t>& inputsDims1 = getInput(1)->dims();
+
+        std::vector<std::size_t> outDims = (inputsDims0.size() >= inputsDims1.size()) ? inputsDims0 : inputsDims1;
+        const std::vector<std::size_t>& lowDims = (inputsDims0.size() < inputsDims1.size()) ? inputsDims0 : inputsDims1;
+
+        std::size_t out_id = outDims.size() - 1;
+        std::size_t low_id = lowDims.size() - 1;
+        std::size_t i = 0;
+        while (i++ < lowDims.size()) {
+            if (outDims[out_id] == 1) {
+                outDims[out_id] = lowDims[low_id];
+            }
+            else if ((lowDims[low_id] != 1) && (lowDims[low_id] != outDims[out_id])) {
+                AIDGE_THROW_OR_ABORT(std::runtime_error, "Unsopported Tensor shape for Div Operation");
+            }
+            --out_id;
+            --low_id;
+        }
+        mOutputs[0]->resize(outDims);
     }
-}
\ No newline at end of file
+}
+
+
+void Aidge::Div_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    SET_IMPL_MACRO(Div_Op, *this, name);
+    mOutputs[0]->setBackend(name, device);
+}
diff --git a/src/operator/Erf.cpp b/src/operator/Erf.cpp
index 387af4edf417f8c7ac6ee9b8b2b7069179ad59cb..81c87f10b10210c2af203a05df53e3330bb33b72 100644
--- a/src/operator/Erf.cpp
+++ b/src/operator/Erf.cpp
@@ -9,8 +9,17 @@
  *
  ********************************************************************************/
 
+#include "aidge/operator/Erf.hpp"
+
 #include <string>
 
-#include "aidge/operator/Erf.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
+
+const std::string Aidge::Erf_Op::Type = "Erf";
 
-const std::string Aidge::Erf_Op::Type = "Erf";
\ No newline at end of file
+void Aidge::Erf_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    SET_IMPL_MACRO(Erf_Op, *this, name);
+    mOutputs[0]->setBackend(name, device);
+}
diff --git a/src/operator/FC.cpp b/src/operator/FC.cpp
index 32114f5bf9e0d160db9fdc2d1971481be0b4e703..9865d64f6a0b87be96244bc4b39c91b605f02b6f 100644
--- a/src/operator/FC.cpp
+++ b/src/operator/FC.cpp
@@ -9,8 +9,52 @@
  *
  ********************************************************************************/
 
+#include "aidge/operator/FC.hpp"
+
+#include <memory>
 #include <string>
+#include <vector>
 
-#include "aidge/operator/FC.hpp"
+#include "aidge/data/Data.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/StaticAttributes.hpp"
+#include "aidge/utils/Types.h"
+
+const std::string Aidge::FC_Op::Type = "FC";
+
+void Aidge::FC_Op::associateInput(const Aidge::IOIndex_t inputIdx, const std::shared_ptr<Aidge::Data>& data) {
+    AIDGE_ASSERT(inputIdx < 3, "Operators {} supports only {} inputs", type(), nbInputs());
+    AIDGE_ASSERT(data->type() == Tensor::Type, "input data must be of Tensor type");
+    // TODO: FIXME: check this, because data dims may not be initialized at this point...
+    //if (inputIdx == 2) {
+    //    assert(std::dynamic_pointer_cast<Tensor>(data)->size() == ((this->template getAttr<FCAttr::NoBias>()) == false ? static_cast<std::size_t>(this->template getAttr<FCAttr::OutChannels>()) : 0));
+    //    assert(std::dynamic_pointer_cast<Tensor>(data)->nbDims() == 1);
+    //}
+    mInputs[inputIdx] = std::dynamic_pointer_cast<Tensor>(data);
+    if (inputIdx == 0 && getInput(0)->nbDims() == 1)
+        mInputs[inputIdx]->resize({1, getInput(inputIdx)->size()});
+}
+
+void Aidge::FC_Op::computeOutputDims() {
+    bool associated = true;
+    for (IOIndex_t i = 0; i < nbInputs(); ++i) {
+        if (!getInput(i)) {
+            AIDGE_THROW_OR_ABORT(std::runtime_error, "{}: input #{} should be associated with a Tensor", type(), i);
+        }
+        associated &= !(getInput(i)->empty());
+    }
+    if (associated) {
+        // <batch, OutChannels>
+        mOutputs[0]->resize({getInput(0)->dims()[0], this->template getAttr<FCAttr::OutChannels>()});
+    }
+}
+
+void Aidge::FC_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    SET_IMPL_MACRO(FC_Op, *this, name);
+    mOutputs[0]->setBackend(name, device);
 
-const std::string Aidge::FC_Op::Type = "FC";
\ No newline at end of file
+    // By default, automatically set backend for weight and bias inputs
+    getInput(1)->setBackend(name, device);
+    getInput(2)->setBackend(name, device);
+}
diff --git a/src/operator/Gather.cpp b/src/operator/Gather.cpp
index 30804994b6084a5a5558f106a38a6087e54471bc..259e6513994970eb7e677f44c981888388825fae 100644
--- a/src/operator/Gather.cpp
+++ b/src/operator/Gather.cpp
@@ -9,31 +9,47 @@
  *
  ********************************************************************************/
 
-#include <cassert>
-#include <cstddef>
+#include "aidge/operator/Gather.hpp"
+
+#include <cstddef>  // std::size_t
+#include <cstdint>  // std::int64_t
 #include <string>
 #include <vector>
 
-#include "aidge/operator/Gather.hpp"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/utils/Types.h"
 #include "aidge/utils/ErrorHandling.hpp"
 
+
 const std::string Aidge::Gather_Op::Type = "Gather";
 
 void Aidge::Gather_Op::computeOutputDims() {
     // check inputs have been associated
-    if (!getInput(0) || !getInput(1)) {
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "At least one input was not connected");
+    if (!getInput(0)) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "Input was not connected");
     }
 
-    if (getInput(1)->nbDims()!=2){
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "Indices input must be a 2D Tensor");
+    if (!getInput(0)->empty()) {
+        std::vector<DimSize_t> outDims = getInput(0)->dims();
+        const std::vector<DimSize_t> gatheredShape = this->template getAttr<GatherAttr::GatheredShape>();
+        // TODO: check indices and gatheredShape
+
+        const std::int64_t axisIdx = this->template getAttr<GatherAttr::Axis>() >= 0 ?
+                                        this->template getAttr<GatherAttr::Axis>() :
+                                        this->template getAttr<GatherAttr::Axis>() + outDims.size();
+        outDims.erase(outDims.begin() + static_cast<std::size_t>(axisIdx));
+        if (!gatheredShape.empty())
+        {
+            outDims.insert(outDims.cbegin() + static_cast<std::size_t>(axisIdx),
+                            gatheredShape.cbegin(),
+                            gatheredShape.cend());
+        }
+
+        mOutputs[0]->resize(outDims);
     }
+}
 
-    std::vector<DimSize_t> outDims = getInput(0)->dims();
-    std::vector<DimSize_t> indexesDims = getInput(1)->dims();
-    int axisIdx = this->template getAttr<GatherAttr::Axis>()>=0?this->template getAttr<GatherAttr::Axis>():this->template getAttr<GatherAttr::Axis>()+outDims.size();
-    outDims.erase(outDims.begin() + static_cast<std::size_t>(axisIdx));
-    outDims.insert(outDims.begin() + static_cast<std::size_t>(axisIdx), indexesDims.begin(),indexesDims.end());
-    mOutputs[0]->resize(outDims);
-}
\ No newline at end of file
+void Aidge::Gather_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    SET_IMPL_MACRO(Gather_Op, *this, name);
+    mOutputs[0]->setBackend(name, device);
+}
diff --git a/src/operator/GenericOperator.cpp b/src/operator/GenericOperator.cpp
index 192036651cfbe2df71139dd63ca3d71f07300964..3eae49b69ce639529d49dd1c0d241f12ece5d98b 100644
--- a/src/operator/GenericOperator.cpp
+++ b/src/operator/GenericOperator.cpp
@@ -9,9 +9,48 @@
  *
  ********************************************************************************/
 
+#include "aidge/operator/GenericOperator.hpp"
+
+#include <cstddef>  // std::size_t
 #include <vector>
 
-#include "aidge/operator/GenericOperator.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/Types.h"
+#include "aidge/utils/ErrorHandling.hpp"
 
 const Aidge::GenericOperator_Op::ComputeDimsFunc Aidge::GenericOperator_Op::Identity
-    = [](const std::vector<std::vector<size_t>>& inputsDims) { return inputsDims; };
+    = [](const std::vector<std::vector<std::size_t>>& inputsDims) { return inputsDims; };
+
+const Aidge::GenericOperator_Op::ComputeDimsFunc Aidge::GenericOperator_Op::InputIdentity(IOIndex_t inputIdx, IOIndex_t nbOutputs) {
+    return [nbOutputs, inputIdx](const std::vector<std::vector<std::size_t>>& inputsDims) { return std::vector<std::vector<std::size_t>>(nbOutputs, inputsDims[inputIdx]); };
+}
+
+void Aidge::GenericOperator_Op::computeOutputDims() {
+    if (mComputeOutputDims) {
+        std::vector<std::vector<std::size_t>> inputsDims(nbInputs(), std::vector<std::size_t>());
+        for (std::size_t i = 0; i < nbInputs(); ++i) {
+            if (getInput(i)) {
+                inputsDims[i] = getInput(i)->dims();
+            }
+        }
+
+        const auto& outputsDims = mComputeOutputDims(inputsDims);
+        AIDGE_ASSERT((outputsDims.size() == nbOutputs()), "The provided ComputeDimsFunc function returns the wrong number of outputs");
+        for (std::size_t i = 0; i < nbOutputs(); ++i) {
+            mOutputs[i]->resize(outputsDims[i]);
+        }
+    }
+    else {
+        AIDGE_ASSERT(false, "Cannot compute output dim of a GenericOperator");
+    }
+}
+
+bool Aidge::GenericOperator_Op::outputDimsForwarded() const {
+    if (mComputeOutputDims) {
+        return !(mOutputs[0]->empty());
+    }
+    else {
+        AIDGE_ASSERT(false, "GenericOperator cannot forward dims");
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/src/operator/GlobalAveragePooling.cpp b/src/operator/GlobalAveragePooling.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..618ccc06f40da4b1f1c491487fd978da768652e4
--- /dev/null
+++ b/src/operator/GlobalAveragePooling.cpp
@@ -0,0 +1,52 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <memory>
+#include <stdexcept>  // std::runtime_error
+#include <string>
+#include <vector>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/operator/GlobalAveragePooling.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Types.h"
+
+const std::string Aidge::GlobalAveragePooling_Op::Type = "GlobalAveragePooling";
+
+void Aidge::GlobalAveragePooling_Op::computeOutputDims() {
+  // error checking
+  if (!getInput(0)) {
+    AIDGE_THROW_OR_ABORT(std::runtime_error,
+                         "GlobalAveragePooling : The input was not connected");
+  }
+  // necessary bc forward dims sometimes passes with an empty vector before
+  // doing another pass
+  else if (getInput(0)->empty()) {
+    return;
+  // computation
+  } else {
+    AIDGE_ASSERT(getInput(0)->dims().size() >= 3,
+                 "GlobalAveragePooling :  needs at least a 3 dimensions input, "
+                 "number of input dim : {}",
+                 getInput(0)->dims().size());
+    // Global average pooling takes each filter, averages its values and uses
+    // it as an output(Much like a fancier flatten). 1st dim is batch 2nd is
+    // number of filter
+    const std::vector<DimSize_t> out_dims{getInput(0)->dims().at(0),
+                                          getInput(0)->dims().at(1)};
+    mOutputs[0]->resize(out_dims);
+  }
+}
+
+void Aidge::GlobalAveragePooling_Op::setBackend(const std::string &name, Aidge::DeviceIdx_t device) {
+    SET_IMPL_MACRO(GlobalAveragePooling_Op, *this, name);
+    mOutputs[0]->setBackend(name, device);
+}
\ No newline at end of file
diff --git a/src/operator/MatMul.cpp b/src/operator/MatMul.cpp
index 666ed3921ed1190a91935bd9f38303e23963d912..56899875338d487294163aa018e0d98b5f7a5269 100644
--- a/src/operator/MatMul.cpp
+++ b/src/operator/MatMul.cpp
@@ -9,8 +9,70 @@
  *
  ********************************************************************************/
 
+#include <algorithm>
 #include <string>
+#include <vector>
 
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/MatMul.hpp"
+#include "aidge/utils/Types.h"
+#include "aidge/utils/ErrorHandling.hpp"
 
-const std::string Aidge::MatMul_Op::Type = "MatMul";
\ No newline at end of file
+const std::string Aidge::MatMul_Op::Type = "MatMul";
+
+void Aidge::MatMul_Op::computeOutputDims() {
+    if (!getInput(0) || !getInput(1)) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "Missing input. Cannot compute output dimensions for MatMul Operator.");
+    }
+    if (getInput(0)->empty() && getInput(1)->empty()) {
+        // both inputs are scalar
+        mOutputs[0]->resize({});
+    }
+    else if (!getInput(0)->empty() && !getInput(1)->empty())
+    {
+        std::vector<std::size_t> dims0 = getInput(0)->dims();
+        std::vector<std::size_t> dims1 = getInput(1)->dims();
+
+        // keep second-to-last dimension of dims0
+        const bool keepDim0 = dims0.size() > 1;
+        // keep last dimension of dims1
+        const bool keepDim1 = dims1.size() > 1;
+
+        if (dims0.size() == 1) {
+            dims0.insert(dims0.cbegin(), 1);
+        }
+        if (dims1.size() == 1) {
+            dims1.push_back(1);
+        }
+        const std::size_t dims_size = std::max(dims0.size(), dims1.size());
+
+
+        if (dims0.size() > dims1.size()) {
+            dims1.insert(dims1.cbegin(), dims0.size() - dims1.size(), std::size_t(1));
+        }
+        else if (dims1.size() > dims0.size()) {
+            dims0.insert(dims0.cbegin(), dims1.size() - dims0.size(), std::size_t(1));
+        }
+
+        AIDGE_ASSERT(dims0[dims_size-1] == dims1[dims_size-2], "Incompatible matrices sizes.");
+
+        std::vector<std::size_t> outDims = std::vector<std::size_t>(dims_size-2, 1);
+        for (std::size_t i = 0; i < dims_size-2; ++i) {
+            AIDGE_ASSERT((dims0[i] == dims1[i]) || (dims0[i] == 1) || (dims1[i] == 1), "Bad vector dimension.");
+            outDims[i] = std::max(dims0[i], dims1[i]);
+        }
+
+        // use keepDim0 instead of dims0.size() because dims0 has been modified
+        if (keepDim0)
+            outDims.push_back(dims0[dims_size-2]);
+        if (keepDim1)
+            outDims.push_back(dims1[dims_size-1]);
+
+        mOutputs[0]->resize(outDims);
+    }
+}
+
+void Aidge::MatMul_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    SET_IMPL_MACRO(MatMul_Op, *this, name);
+    mOutputs[0]->setBackend(name, device);
+}
diff --git a/src/operator/Memorize.cpp b/src/operator/Memorize.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6e54a234d2fc78c8e8e9a43a7528709c8e51adc4
--- /dev/null
+++ b/src/operator/Memorize.cpp
@@ -0,0 +1,69 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include "aidge/operator/Memorize.hpp"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Types.h"
+
+const std::string Aidge::Memorize_Op::Type = "Memorize";
+
+void Aidge::Memorize_Op::computeOutputDims() {
+    for (size_t i = 0; i < 2; ++i) {
+        if (!getInput(i)) {
+            AIDGE_THROW_OR_ABORT(std::runtime_error, "{}: input #{} should be associated with a Tensor", type(), i);
+        }
+    }
+
+    // Only require one of the input to have dims defined
+    // Otherwise, forwardDims() won't converge!
+    if (!(getInput(0)->empty())) {
+        const auto expectedDims =  getInput(0)->dims();
+        mOutputs[0]->resize(expectedDims);
+    }
+    else if (!(getInput(1)->empty())) {
+        const auto expectedDims =  getInput(1)->dims();
+        mOutputs[0]->resize(expectedDims);
+    }
+}
+
+void Aidge::Memorize_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    mImpl = Registrar<Memorize_Op>::create({name})(*this);
+    mOutputs[0]->setBackend(name, device);
+}
+
+bool Aidge::Memorize_Op::outputDimsForwarded() const {
+    // Only check the output dims
+    bool forwarded = true;
+    // check outputs have been filled
+    for (IOIndex_t i = 0; i < nbOutputs(); ++i) {
+        forwarded &= !(getOutput(i)->empty());
+    }
+    return forwarded;
+}
+
+void Aidge::Memorize_Op::updateConsummerProducer() {
+    Operator::updateConsummerProducer();
+    ++this->template getAttr<MemorizeAttr::ScheduleStep>();
+    this->template getAttr<MemorizeAttr::ForwardStep>() = 0;
+}
+
+void Aidge::Memorize_Op::forward() {
+    Operator::forward();
+    ++this->template getAttr<MemorizeAttr::ForwardStep>();
+    this->template getAttr<MemorizeAttr::ScheduleStep>() = 0;
+}
diff --git a/src/operator/MetaOperator.cpp b/src/operator/MetaOperator.cpp
index 530357085a16ca3e834669cebd2d26882ca8ddab..46e9e1173af98ed5711aa0bbce54705fb61dc03c 100644
--- a/src/operator/MetaOperator.cpp
+++ b/src/operator/MetaOperator.cpp
@@ -10,13 +10,20 @@
  ********************************************************************************/
 
 #include "aidge/operator/MetaOperator.hpp"
+
+#include <cstddef>  // std::size_t
+#include <memory>
+#include <string>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/graph/GraphView.hpp"
 #include "aidge/utils/ErrorHandling.hpp"
 
-Aidge::MetaOperator_Op::MetaOperator_Op(const char *type, const std::shared_ptr<GraphView>& graph)
-    : OperatorTensor(type, graph->dataInputs().size(), (graph->inputs().size() - graph->dataInputs().size()), graph->outputs().size()),
+Aidge::MetaOperator_Op::MetaOperator_Op(const std::string& type, const std::shared_ptr<GraphView>& graph)
+    : OperatorTensor(type, graph->dataInputs().size(), (graph->getOrderedInputs().size() - graph->dataInputs().size()), graph->getOrderedOutputs().size()),
         mGraph(graph)
 {
-    mInputs = std::vector<std::shared_ptr<Tensor>>(mGraph->inputs().size());
+    mInputs = std::vector<std::shared_ptr<Tensor>>(mGraph->getOrderedInputs().size());
     for (std::size_t i = 0; i < mInputs.size(); ++i) {
         mInputs[i] = std::make_shared<Tensor>();
     }
@@ -24,37 +31,84 @@ Aidge::MetaOperator_Op::MetaOperator_Op(const char *type, const std::shared_ptr<
     mOutputs = std::vector<std::shared_ptr<Tensor>>(mGraph->getOrderedOutputs().size());
     for (size_t outputIdx = 0; outputIdx < mOutputs.size(); ++outputIdx) {
         const auto& outputOp = mGraph->getOrderedOutputs()[outputIdx];
-        mOutputs[outputIdx] = std::dynamic_pointer_cast<Tensor>(outputOp.first->getOperator()->getRawOutput(outputOp.second));
+        if (outputOp.first) {
+            mOutputs[outputIdx] = std::dynamic_pointer_cast<Tensor>(outputOp.first->getOperator()->getRawOutput(outputOp.second));
+        }
     }
 }
 
-Aidge::NbElts_t Aidge::MetaOperator_Op::getNbRequiredData(const IOIndex_t inputIdx) const {
+Aidge::Elts_t Aidge::MetaOperator_Op::getNbRequiredData(const IOIndex_t inputIdx) const {
     if (mImpl) {
         return mImpl->getNbRequiredData(inputIdx);
     }
     else {
         const auto& inputOp = mGraph->getOrderedInputs()[inputIdx];
-        return inputOp.first->getOperator()->getNbRequiredData(inputOp.second);
+        if (inputOp.first) {
+            return inputOp.first->getOperator()->getNbRequiredData(inputOp.second);
+        }
+        else {
+            return Elts_t::NoneElts();
+        }
+    }
+}
+
+Aidge::Elts_t Aidge::MetaOperator_Op::getNbRequiredProtected(const IOIndex_t inputIdx) const {
+    if (mImpl) {
+        return mImpl->getNbRequiredProtected(inputIdx);
+    }
+    else {
+        const auto& inputOp = mGraph->getOrderedInputs()[inputIdx];
+        if (inputOp.first) {
+            return inputOp.first->getOperator()->getNbRequiredProtected(inputOp.second);
+        }
+        else {
+            return Elts_t::NoneElts();
+        }
     }
 }
 
-Aidge::NbElts_t Aidge::MetaOperator_Op::getNbConsumedData(IOIndex_t inputIdx) const {
+Aidge::Elts_t Aidge::MetaOperator_Op::getRequiredMemory(const IOIndex_t outputIdx, const std::vector<DimSize_t> &inputsSize) const {
+    if (mImpl) {
+        return mImpl->getRequiredMemory(outputIdx, inputsSize);
+    }
+    else {
+        const auto& outputOp = mGraph->getOrderedOutputs()[outputIdx];
+        if (outputOp.first) {
+            return outputOp.first->getOperator()->getRequiredMemory(outputOp.second, inputsSize);
+        }
+        else {
+            return Elts_t::NoneElts();
+        }
+    }
+}
+
+Aidge::Elts_t Aidge::MetaOperator_Op::getNbConsumedData(IOIndex_t inputIdx) const {
     if (mImpl) {
         return mImpl->getNbConsumedData(inputIdx);
     }
     else {
         const auto& inputOp = mGraph->getOrderedInputs()[inputIdx];
-        return inputOp.first->getOperator()->getNbConsumedData(inputOp.second);
+        if (inputOp.first) {
+            return inputOp.first->getOperator()->getNbConsumedData(inputOp.second);
+        }
+        else {
+            return Elts_t::NoneElts();
+        }
     }
 }
 
-Aidge::NbElts_t Aidge::MetaOperator_Op::getNbProducedData(IOIndex_t outputIdx) const {
+Aidge::Elts_t Aidge::MetaOperator_Op::getNbProducedData(IOIndex_t outputIdx) const {
     if (mImpl) {
         return mImpl->getNbProducedData(outputIdx);
     }
     else {
         const auto& outputOp = mGraph->getOrderedOutputs()[outputIdx];
-        return outputOp.first->getOperator()->getNbProducedData(outputOp.second);
+        if (outputOp.first) {
+            return outputOp.first->getOperator()->getNbProducedData(outputOp.second);
+        }
+        else {
+            return Elts_t::NoneElts();
+        }
     }
 }
 
@@ -65,10 +119,9 @@ void Aidge::MetaOperator_Op::updateConsummerProducer() {
     else {
         if (!mScheduler) {
             // Lazy initialization
-            mScheduler = std::make_shared<SequentialScheduler>(mGraph);
+            mScheduler = std::make_shared<SequentialScheduler>(mGraph, mUpperNode.lock());
         }
 
-
         // TODO: check that generateScheduling() can be called multiple time to iteratively update the schedule.
         // It could be a good idea to unify updateConsummerProducer() and generateScheduling() into a "updateScheduling()"
         mScheduler->generateScheduling();
@@ -86,7 +139,7 @@ void Aidge::MetaOperator_Op::forward() {
             // Lazy initialization
             // TODO: should we assert that a scheduler already exists at this point?
             // => should be created in updateConsummerProducer()
-            mScheduler = std::make_shared<SequentialScheduler>(mGraph);
+            mScheduler = std::make_shared<SequentialScheduler>(mGraph, mUpperNode.lock());
             mScheduler->generateScheduling();
         }
 
diff --git a/src/operator/Mul.cpp b/src/operator/Mul.cpp
index bc268263e8a6e2ec7c9944faa31da84dc50c4f53..89bef9e0edcf6731dfbaf9ebf48ebddf5b71e815 100644
--- a/src/operator/Mul.cpp
+++ b/src/operator/Mul.cpp
@@ -9,15 +9,17 @@
  *
  ********************************************************************************/
 
-#include <cassert>
-#include <cstddef>
+#include <cstddef>    // std::size_t
+#include <memory>
+#include <stdexcept>  // std::runtime_error
+#include <string>
 #include <vector>
-#include <utility>
 
 #include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Mul.hpp"
-#include "aidge/utils/Types.h"
 #include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Types.h"
 
 const std::string Aidge::Mul_Op::Type = "Mul";
 
@@ -27,11 +29,35 @@ void Aidge::Mul_Op::computeOutputDims() {
         AIDGE_THROW_OR_ABORT(std::runtime_error, "At least one input was not connected");
     }
 
-    if ((!getInput(0)->empty()) &&
-        ((getInput(1)->size() == 1) || // mul by a single value
-        (getInput(1)->size() == getInput(0)->size()) || // mul elem-wise
-        (getInput(1)->nbDims() == 1 && getInput(1)->size() == getInput(0)->dims()[getInput(0)->nbDims()-1]))) // mul by a Tensor with one dimension of output size
-    {
-        mOutputs[0]->resize(getInput(0)->dims());
+    if (!getInput(0)->empty() && !getInput(1)->empty()) {
+
+        const std::vector<std::size_t>& inputsDims0 = getInput(0)->dims();
+        const std::vector<std::size_t>& inputsDims1 = getInput(1)->dims();
+
+        std::vector<std::size_t> outDims = (inputsDims0.size() >= inputsDims1.size()) ? inputsDims0 : inputsDims1;
+        const std::vector<std::size_t>& lowDims = (inputsDims0.size() < inputsDims1.size()) ? inputsDims0 : inputsDims1;
+
+        std::size_t out_id = outDims.size() - 1;
+        std::size_t low_id = lowDims.size() - 1;
+        std::size_t i = 0;
+        while (i++ < lowDims.size()) {
+            if (outDims[out_id] == 1) {
+                outDims[out_id] = lowDims[low_id];
+            }
+            else if ((lowDims[low_id] != 1) && (lowDims[low_id] != outDims[out_id])) {
+                AIDGE_THROW_OR_ABORT(std::runtime_error, "Unsopported Tensor shape for Div Operation");
+            }
+            --out_id;
+            --low_id;
+        }
+        mOutputs[0]->resize(outDims);
+    }
+    else if (!getInput(0)->empty() && !getInput(1)->empty()) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "Incompatible input dimensions for Operator Mul: {} and {}", getInput(0)->dims(), getInput(1)->dims());
     }
-}
\ No newline at end of file
+}
+
+void Aidge::Mul_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    SET_IMPL_MACRO(Mul_Op, *this, name);
+    mOutputs[0]->setBackend(name, device);
+}
diff --git a/src/operator/Operator.cpp b/src/operator/Operator.cpp
index 4adc57f55f7531c28c0c0603ee01c176bdd59e96..317bbd364572f49a714e328bf33f3cd58c19215f 100644
--- a/src/operator/Operator.cpp
+++ b/src/operator/Operator.cpp
@@ -31,20 +31,38 @@ Aidge::Operator::~Operator() noexcept = default;
 //        IMPLEMENTATION
 ///////////////////////////////////////////////////////
 
-Aidge::NbElts_t Aidge::Operator::getNbRequiredData(const Aidge::IOIndex_t inputIdx) const {
+Aidge::Elts_t Aidge::Operator::getNbRequiredData(const Aidge::IOIndex_t inputIdx) const {
+    AIDGE_ASSERT(mImpl != nullptr, "getNbRequiredData(): an implementation is required for {}!", type());
     return mImpl->getNbRequiredData(inputIdx);
 }
 
-Aidge::NbElts_t Aidge::Operator::getNbConsumedData(Aidge::IOIndex_t inputIdx) const {
+Aidge::Elts_t Aidge::Operator::getNbRequiredProtected(const Aidge::IOIndex_t inputIdx) const {
+    AIDGE_ASSERT(mImpl != nullptr, "getNbRequiredProtected(): an implementation is required for {}!", type());
+    return mImpl->getNbRequiredProtected(inputIdx);
+}
+
+Aidge::Elts_t Aidge::Operator::getRequiredMemory(const IOIndex_t outputIdx, const std::vector<DimSize_t> &inputsSize) const {
+    AIDGE_ASSERT(mImpl != nullptr, "getRequiredMemory(): an implementation is required for {}!", type());
+    return mImpl->getRequiredMemory(outputIdx, inputsSize);
+}
+
+Aidge::Elts_t Aidge::Operator::getNbConsumedData(Aidge::IOIndex_t inputIdx) const {
+    AIDGE_ASSERT(mImpl != nullptr, "getNbConsumedData(): an implementation is required for {}!", type());
     return mImpl->getNbConsumedData(inputIdx);
 }
 
-Aidge::NbElts_t Aidge::Operator::getNbProducedData(Aidge::IOIndex_t outputIdx) const {
+Aidge::Elts_t Aidge::Operator::getNbProducedData(Aidge::IOIndex_t outputIdx) const {
+    AIDGE_ASSERT(mImpl != nullptr, "getNbProducedData(): an implementation is required for {}!", type());
     return mImpl->getNbProducedData(outputIdx);
 }
 void Aidge::Operator::updateConsummerProducer(){
+    AIDGE_ASSERT(mImpl != nullptr, "updateConsummerProducer(): an implementation is required for {}!", type());
     mImpl->updateConsummerProducer();
 }
+void Aidge::Operator::resetConsummerProducer(){
+    AIDGE_ASSERT(mImpl != nullptr, "resetConsummerProducer(): an implementation is required for {}!", type());
+    mImpl->resetConsummerProducer();
+}
 
 void Aidge::Operator::runHooks() const {
     for (auto& hook : mHooks) {
@@ -52,12 +70,12 @@ void Aidge::Operator::runHooks() const {
     }
 }
 void Aidge::Operator::forward() {
-    if(mImpl) {
-        mImpl->forward();
-        runHooks();
-    } else {
-        printf("forward: No implementation is linked.\n");
-    }
+    AIDGE_ASSERT(mImpl != nullptr, "forward(): an implementation is required for {}!", type());
+    mImpl->forward();
+    runHooks();
 }
 
-void Aidge::Operator::backward() { mImpl->backward(); }
+void Aidge::Operator::backward() {
+    AIDGE_ASSERT(mImpl != nullptr, "backward(): an implementation is required for {}!", type());
+    mImpl->backward(); 
+}
diff --git a/src/operator/OperatorTensor.cpp b/src/operator/OperatorTensor.cpp
index 72a71814b1463395443c6a4504f2eef660ec1185..b85c18040ad84a1e9b1ea1f8b475c32260b6587a 100644
--- a/src/operator/OperatorTensor.cpp
+++ b/src/operator/OperatorTensor.cpp
@@ -19,20 +19,40 @@
 #include "aidge/utils/ErrorHandling.hpp"
 
 
-void Aidge::OperatorTensor::associateInput(const Aidge::IOIndex_t inputIdx, const std::shared_ptr<Aidge::Data>& data) {
-    if (inputIdx >= nbInputs()) {
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "%s Operator has %hu inputs", type().c_str(), nbInputs());
+Aidge::OperatorTensor::OperatorTensor(const std::string& type,
+                                                            const IOIndex_t nbData,
+                                                            const IOIndex_t nbParam,
+                                                            const IOIndex_t nbOut)
+: Operator(type, nbData, nbParam, nbOut, OperatorType::Tensor),
+        mInputs(std::vector<std::shared_ptr<Tensor>>(nbData + nbParam, nullptr)),
+        mOutputs(std::vector<std::shared_ptr<Tensor>>(nbOut)) {
+    for (std::size_t i = 0; i < static_cast<std::size_t>(nbOut); ++i) {
+        mOutputs[i] = std::make_shared<Tensor>();
+        mOutputs[i]->setDataType(DataType::Float32);
     }
-    if (strcmp((data)->type(), Tensor::Type) != 0) {
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "Input data must be of Tensor type");
+}
+
+
+Aidge::OperatorTensor::OperatorTensor(const OperatorTensor& other)
+    : Operator(other),
+        mInputs(std::vector<std::shared_ptr<Tensor>>(other.nbInputs(), nullptr)),
+        mOutputs(std::vector<std::shared_ptr<Tensor>>(other.nbOutputs())) {
+    for (std::size_t i = 0; i < static_cast<std::size_t>(nbOutputs()); ++i) {
+        mOutputs[i] = std::make_shared<Tensor>();
+        // mOutputs[i] = std::make_shared<Tensor>(*(other.getOutput(i)));
+        // datatype already copied
     }
+}
+
+
+void Aidge::OperatorTensor::associateInput(const Aidge::IOIndex_t inputIdx, const std::shared_ptr<Aidge::Data>& data) {
+    AIDGE_ASSERT(inputIdx < nbInputs(), "{} Operator has {} inputs", type(), nbInputs());
+    AIDGE_ASSERT(data->type() == Tensor::Type, "Input data must be of Tensor type");
     mInputs[inputIdx] = std::dynamic_pointer_cast<Tensor>(data);
 }
 
 void Aidge::OperatorTensor::setInput(const Aidge::IOIndex_t inputIdx, const std::shared_ptr<Aidge::Data>& data) {
-    if (strcmp(data->type(), "Tensor") != 0) {
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "%s Operator only accepts Tensors as inputs", type().c_str());
-    }
+    AIDGE_ASSERT(data->type() == Tensor::Type, "{} Operator only accepts Tensors as inputs", type());
     if (getInput(inputIdx)) {
         *mInputs[inputIdx] = *std::dynamic_pointer_cast<Tensor>(data);
     } else {
@@ -43,9 +63,7 @@ void Aidge::OperatorTensor::setInput(const Aidge::IOIndex_t inputIdx, const std:
 Aidge::OperatorTensor::~OperatorTensor() = default;
 
 void Aidge::OperatorTensor::setInput(const Aidge::IOIndex_t inputIdx, std::shared_ptr<Aidge::Data>&& data) {
-    if (strcmp(data->type(), "Tensor") != 0) {
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "%s Operator only accepts Tensors as inputs", type().c_str());
-    }
+    AIDGE_ASSERT(data->type() == Tensor::Type, "{} Operator only accepts Tensors as inputs", type());
     if (getInput(inputIdx)) {
         *mInputs[inputIdx] = std::move(*std::dynamic_pointer_cast<Tensor>(data));
     } else {
@@ -53,37 +71,38 @@ void Aidge::OperatorTensor::setInput(const Aidge::IOIndex_t inputIdx, std::share
     }
 }
 
+std::shared_ptr<Aidge::Data> Aidge::OperatorTensor::getRawInput(const Aidge::IOIndex_t inputIdx) const {
+    return std::static_pointer_cast<Data>(getInput(inputIdx));
+}
 const std::shared_ptr<Aidge::Tensor>& Aidge::OperatorTensor::getInput(const Aidge::IOIndex_t inputIdx) const {
-    if (inputIdx >= nbInputs()) {
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "%s Operator has %hu inputs", type().c_str(), nbInputs());
-    }
+    AIDGE_ASSERT(inputIdx < nbInputs(), "{} Operator has {} inputs", type(), nbInputs());
     return mInputs[inputIdx];
 }
 
 void Aidge::OperatorTensor::setOutput(const Aidge::IOIndex_t outputIdx, const std::shared_ptr<Aidge::Data>& data) {
-    if (strcmp(data->type(), "Tensor") != 0) {
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "%s Operator only accepts Tensors as inputs", type().c_str());
-    }
-    if (outputIdx >= nbOutputs()) {
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "%s Operator has %hu outputs", type().c_str(), nbOutputs());
-    }
-    *mOutputs[outputIdx] = *std::dynamic_pointer_cast<Tensor>(data);
+    AIDGE_ASSERT(data->type() == Tensor::Type, "{} Operator only accepts Tensors as inputs", type());
+    AIDGE_ASSERT(outputIdx < nbOutputs(), "{} Operator has {} outputs", type(), nbOutputs());
+    const auto& data_tensor = std::dynamic_pointer_cast<Tensor>(data);
+    // if (mImpl)
+    //     AIDGE_ASSERT(data_tensor->getImpl()->backend() == backend(), "Data parameter and Operator have different backends: {} and {}", data_tensor->getImpl()->backend(), backend());
+    *mOutputs[outputIdx] = *data_tensor;
 }
 
 void Aidge::OperatorTensor::setOutput(const Aidge::IOIndex_t outputIdx, std::shared_ptr<Aidge::Data>&& data) {
-    if (strcmp(data->type(), "Tensor") != 0) {
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "%s Operator only accepts Tensors as inputs", type().c_str());
-    }
-    if (outputIdx >= nbOutputs()) {
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "%s Operator has %hu outputs", type().c_str(), nbOutputs());
-    }
-    *mOutputs[outputIdx] = std::move(*std::dynamic_pointer_cast<Tensor>(data));
+    AIDGE_ASSERT(data->type() == Tensor::Type, "{} Operator only accepts Tensors as inputs", type());
+    AIDGE_ASSERT(outputIdx < nbOutputs(), "{} Operator has {} outputs", type(), nbOutputs());
+    auto&& data_tensor = std::dynamic_pointer_cast<Tensor>(data);
+    // if (mImpl)
+    //     AIDGE_ASSERT(data_tensor->getImpl()->backend() == backend(), "Data parameter and Operator have different backends: {} and {}", data_tensor->getImpl()->backend(), backend());
+    *mOutputs[outputIdx] = std::move(*data_tensor);
+}
+
+std::shared_ptr<Aidge::Data> Aidge::OperatorTensor::getRawOutput(const Aidge::IOIndex_t outputIdx) const {
+    return std::static_pointer_cast<Data>(getOutput(outputIdx));
 }
 
 const std::shared_ptr<Aidge::Tensor>& Aidge::OperatorTensor::getOutput(const Aidge::IOIndex_t outputIdx) const {
-    if (outputIdx >= nbOutputs()) {
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "%s Operator has %hu outputs", type().c_str(), nbOutputs());
-    }
+    AIDGE_ASSERT(outputIdx < nbOutputs(), "{} Operator has {} outputs", type(), nbOutputs());
     return mOutputs[outputIdx];
 }
 
@@ -105,7 +124,7 @@ std::vector<std::pair<std::vector<Aidge::DimSize_t>, std::vector<Aidge::DimSize_
     }
     for (DimIdx_t i = 0; i < outputDims.size(); ++i) {
         if (((outputDims[i] + firstEltDims[i]) > getOutput(0)->dims()[i]) || (outputDims[i] == 0)) {
-            AIDGE_THROW_OR_ABORT(std::runtime_error, "Given outputDim out of range for dimension %lu (%lu + %lu)", static_cast<std::size_t>(i), firstEltDims[i], outputDims[i]);
+            AIDGE_THROW_OR_ABORT(std::runtime_error, "Given outputDim out of range for dimension {} ({} + {})", static_cast<std::size_t>(i), firstEltDims[i], outputDims[i]);
         }
     }
     // return the same Tensor description as given in function parameter for each data input
@@ -117,7 +136,7 @@ void Aidge::OperatorTensor::computeOutputDims() {
     bool associated = (nbInputs() > 0); // do not compute anything if no input
     for (IOIndex_t i = 0; i < nbInputs(); ++i) {
         if (!getInput(i)) {
-            AIDGE_THROW_OR_ABORT(std::runtime_error, "Every input should be associated with a Tensor");
+            AIDGE_THROW_OR_ABORT(std::runtime_error, "{}: input #{} should be associated with a Tensor", type(), i);
         }
         associated &= !(getInput(i)->empty());
     }
@@ -125,7 +144,9 @@ void Aidge::OperatorTensor::computeOutputDims() {
         const auto expectedDims =  getInput(0)->dims();
         for (std::size_t i = 1; i < nbInputs(); ++i) {
             if (expectedDims != getInput(i)->dims()) {
-                AIDGE_THROW_OR_ABORT(std::runtime_error, "Operator's inputs should have the same dimensions");
+                AIDGE_THROW_OR_ABORT(std::runtime_error,
+                    "{} operator's inputs should have the same dimensions: expected {} (input #0), given {} (input #{})",
+                    type(), expectedDims, getInput(i)->dims(), i);
             }
         }
         mOutputs[0]->resize(expectedDims);
@@ -139,7 +160,9 @@ bool Aidge::OperatorTensor::outputDimsForwarded() const {
         forwarded &= mInputs[i] ? !(getInput(i)->empty()) : false;
     }
     for (IOIndex_t i = 0; i < nbOutputs(); ++i) {
-        forwarded &= !(getOutput(i)->empty());
+        // If getOutput(i) is nullptr, ignore this output (it may be a dummy
+        // output in a MetaOperator)
+        forwarded &= (getOutput(i)) ? !(getOutput(i)->empty()) : true;
     }
     return forwarded;
 }
@@ -150,6 +173,7 @@ void Aidge::OperatorTensor::setDataType(const DataType& dataType) const {
     }
 
     for (IOIndex_t i = nbData(); i < nbInputs(); ++i) {
+        AIDGE_ASSERT(getInput(i) != nullptr, "Missing input#{} for operator {}", i, type());
         getInput(i)->setDataType(dataType);
     }
 }
\ No newline at end of file
diff --git a/src/operator/Pop.cpp b/src/operator/Pop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..06999e301ce0968b2d9979e47f412c02e59de3ad
--- /dev/null
+++ b/src/operator/Pop.cpp
@@ -0,0 +1,51 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include "aidge/operator/Pop.hpp"
+
+#include <memory>
+#include <string>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/StaticAttributes.hpp"
+#include "aidge/utils/Types.h"
+
+
+const std::string Aidge::Pop_Op::Type = "Pop";
+
+void Aidge::Pop_Op::computeOutputDims() {
+    // check inputs have been associated
+    if (!getInput(0)) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "{}: input #0 should be associated with a Tensor", type());
+    }
+    if (!(getInput(0)->empty())) {
+        auto inputDims = getInput(0)->dims();
+        inputDims.erase(inputDims.begin());
+        getOutput(0)->resize(inputDims);
+    }
+}
+
+void Aidge::Pop_Op::updateConsummerProducer() {
+    Operator::updateConsummerProducer();
+    this->template getAttr<PopAttr::ForwardStep>() = 0;
+}
+
+void Aidge::Pop_Op::forward() {
+    Operator::forward();
+    ++this->template getAttr<PopAttr::ForwardStep>();
+}
+
+void Aidge::Pop_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    SET_IMPL_MACRO(Pop_Op, *this, name);
+    mOutputs[0]->setBackend(name, device);
+}
diff --git a/src/operator/Pow.cpp b/src/operator/Pow.cpp
index de1f0c3694f51fbd5b365573f61d3e3e2b9109ff..72a04de04fda8a432309de8b4a69b1dfb6af1370 100644
--- a/src/operator/Pow.cpp
+++ b/src/operator/Pow.cpp
@@ -9,12 +9,13 @@
  *
  ********************************************************************************/
 
-#include <cassert>
-#include <cstddef>
+#include <cstddef>    // std::size_t
+#include <stdexcept>  // std::runtime_error
+#include <string>
 #include <vector>
-#include <utility>
 
 #include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/operator/Pow.hpp"
 #include "aidge/utils/Types.h"
 #include "aidge/utils/ErrorHandling.hpp"
@@ -27,11 +28,32 @@ void Aidge::Pow_Op::computeOutputDims() {
         AIDGE_THROW_OR_ABORT(std::runtime_error, "At least one input was not connected");
     }
 
-    if ((!getInput(0)->empty()) &&
-        ((getInput(1)->size() == 1) || // pow by a single value
-        (getInput(1)->size() == getInput(0)->size()) || // pow elem-wise
-        (getInput(1)->nbDims() == 1 && getInput(1)->size() == getInput(0)->dims()[getInput(0)->nbDims()-1]))) // pow by a Tensor with one dimension of output size
-    {
-        mOutputs[0]->resize(getInput(0)->dims());
+    if (!getInput(0)->empty() && !getInput(1)->empty()) {
+
+        const std::vector<std::size_t>& inputsDims0 = getInput(0)->dims();
+        const std::vector<std::size_t>& inputsDims1 = getInput(1)->dims();
+
+        std::vector<std::size_t> outDims = (inputsDims0.size() >= inputsDims1.size()) ? inputsDims0 : inputsDims1;
+        const std::vector<std::size_t>& lowDims = (inputsDims0.size() < inputsDims1.size()) ? inputsDims0 : inputsDims1;
+
+        std::size_t out_id = outDims.size() - 1;
+        std::size_t low_id = lowDims.size() - 1;
+        std::size_t i = 0;
+        while (i++ < lowDims.size()) {
+            if (outDims[out_id] == 1) {
+                outDims[out_id] = lowDims[low_id];
+            }
+            else if ((lowDims[low_id] != 1) && (lowDims[low_id] != outDims[out_id])) {
+                AIDGE_THROW_OR_ABORT(std::runtime_error, "Unsopported Tensor shape for Div Operation");
+            }
+            --out_id;
+            --low_id;
+        }
+        mOutputs[0]->resize(outDims);
     }
+}
+
+void Aidge::Pow_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    SET_IMPL_MACRO(Pow_Op, *this, name);
+    mOutputs[0]->setBackend(name, device);
 }
\ No newline at end of file
diff --git a/src/operator/Producer.cpp b/src/operator/Producer.cpp
index 7bccbe763b90f2697997a889b30b610e4b531334..38bbbc14846f8f4356602b1d3a66058439bb37d0 100644
--- a/src/operator/Producer.cpp
+++ b/src/operator/Producer.cpp
@@ -9,8 +9,114 @@
  *
  ********************************************************************************/
 
+#include "aidge/operator/Producer.hpp"
+
+#include <cstddef>
+#include <array>
+#include <memory>
 #include <string>
 
-#include "aidge/operator/Producer.hpp"
+#include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/StaticAttributes.hpp"
+#include "aidge/utils/Types.h"
+
 
 const std::string Aidge::Producer_Op::Type = "Producer";
+
+
+Aidge::Producer_Op::Producer_Op(const std::shared_ptr<Aidge::Tensor> tensor, bool constant)
+    : OperatorTensor(Type, 0, 0, 1),
+      Attributes_(attr<ProdAttr::Constant>(constant))
+{
+    mOutputs[0] = tensor; // copy the pointer of the Tensor
+#ifdef PYBIND
+    if(Py_IsInitialized()) {
+        auto obj = py::cast(&(*this));
+        setImpl((mOutputs[0]->hasImpl()) ?
+            (Registrar<Producer_Op>::exists({mOutputs[0]->getImpl()->backend()}) ?
+                Registrar<Producer_Op>::create(mOutputs[0]->getImpl()->backend())(*this) :
+                std::make_shared<OperatorImpl>(*this, mOutputs[0]->getImpl()->backend())) :
+            std::make_shared<OperatorImpl>(*this, ""));
+    } else {
+        setImpl((mOutputs[0]->hasImpl()) ?
+            (Registrar<Producer_Op>::exists({mOutputs[0]->getImpl()->backend()}) ?
+                Registrar<Producer_Op>::create(mOutputs[0]->getImpl()->backend())(*this) :
+                std::make_shared<OperatorImpl>(*this, mOutputs[0]->getImpl()->backend())) :
+            std::make_shared<OperatorImpl>(*this, ""));
+    }
+#else
+    setImpl((mOutputs[0]->hasImpl()) ?
+                (Registrar<Producer_Op>::exists({mOutputs[0]->getImpl()->backend()}) ?
+                    Registrar<Producer_Op>::create(mOutputs[0]->getImpl()->backend())(*this) :
+                    std::make_shared<OperatorImpl>(*this, mOutputs[0]->getImpl()->backend())) :
+                std::make_shared<OperatorImpl>(*this, ""));
+#endif
+}
+
+/**
+ * @brief Copy-constructor. Copy the operator attributes and its output tensor(s),
+ * but not its input tensors (the new operator has no input associated).
+ * @param op OperatorTensor to copy.
+ */
+Aidge::Producer_Op::Producer_Op(const Aidge::Producer_Op& op)
+    : OperatorTensor(op),
+      Attributes_(op)
+{
+    mOutputs[0] = std::make_shared<Tensor>(*(op.getOutput(0)));
+#ifdef PYBIND
+    if(Py_IsInitialized()) {
+            auto obj = py::cast(&(*this));
+            setImpl((mOutputs[0]->hasImpl()) ?
+                (Registrar<Producer_Op>::exists({mOutputs[0]->getImpl()->backend()}) ?
+                    Registrar<Producer_Op>::create(mOutputs[0]->getImpl()->backend())(*this) :
+                    std::make_shared<OperatorImpl>(*this, mOutputs[0]->getImpl()->backend())) :
+                std::make_shared<OperatorImpl>(*this, ""));
+        } else {
+            setImpl((mOutputs[0]->hasImpl()) ?
+                (Registrar<Producer_Op>::exists({mOutputs[0]->getImpl()->backend()}) ?
+                    Registrar<Producer_Op>::create(mOutputs[0]->getImpl()->backend())(*this) :
+                    std::make_shared<OperatorImpl>(*this, mOutputs[0]->getImpl()->backend())) :
+                std::make_shared<OperatorImpl>(*this, ""));
+        }
+#else
+    setImpl((mOutputs[0]->hasImpl()) ?
+                (Registrar<Producer_Op>::exists({mOutputs[0]->getImpl()->backend()}) ?
+                    Registrar<Producer_Op>::create(mOutputs[0]->getImpl()->backend())(*this) :
+                    std::make_shared<OperatorImpl>(*this, mOutputs[0]->getImpl()->backend())) :
+                std::make_shared<OperatorImpl>(*this, ""));
+#endif
+    // if (mOutputs[0]->hasImpl()) {
+        // if (Registrar<Producer_Op>::exists({mOutputs[0]->getImpl()->backend()})){
+        //     setImpl(Registrar<Producer_Op>::create(mOutputs[0]->getImpl()->backend())(*this));
+        // }
+        // else  {
+        //     mImpl = std::make_shared<OperatorImpl>(*this, mOutputs[0]->getImpl()->backend());
+        // }
+
+    // } else {
+    //     mImpl = nullptr;
+    // }
+}
+
+void Aidge::Producer_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+#ifdef PYBIND
+    if(Py_IsInitialized()) {
+            auto obj = py::cast(&(*this));
+            setImpl((Registrar<Producer_Op>::exists({name})) ?
+                    Registrar<Producer_Op>::create(name)(*this) :
+                    std::make_shared<OperatorImpl>(*this, ""));
+        } else {
+            setImpl((Registrar<Producer_Op>::exists({name})) ?
+                    Registrar<Producer_Op>::create(name)(*this) :
+                    std::make_shared<OperatorImpl>(*this, ""));
+        }
+#else
+    setImpl((Registrar<Producer_Op>::exists({name})) ?
+        Registrar<Producer_Op>::create(name)(*this) :
+        std::make_shared<OperatorImpl>(*this, ""));
+#endif
+    mOutputs[0]->setBackend(name, device);
+}
\ No newline at end of file
diff --git a/src/operator/ReLU.cpp b/src/operator/ReLU.cpp
index 0f7874acfe7d865ea8c56d4bca02b51864480df6..7b945a7d62ab0ef7f73a25f6f74430e725d17b48 100644
--- a/src/operator/ReLU.cpp
+++ b/src/operator/ReLU.cpp
@@ -9,8 +9,17 @@
  *
  ********************************************************************************/
 
+#include "aidge/operator/ReLU.hpp"
+
+#include <memory>
 #include <string>
 
-#include "aidge/operator/ReLU.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/Types.h"
+
+const std::string Aidge::ReLU_Op::Type = "ReLU";
 
-const std::string Aidge::ReLU_Op::Type = "ReLU";
\ No newline at end of file
+void Aidge::ReLU_Op::setBackend(const std::string& name, DeviceIdx_t device) {
+    SET_IMPL_MACRO(ReLU_Op, *this, name);
+    mOutputs[0]->setBackend(name, device);
+}
\ No newline at end of file
diff --git a/src/operator/ReduceMean.cpp b/src/operator/ReduceMean.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0de676e22ec668a9b41d7d61f184465d431715a2
--- /dev/null
+++ b/src/operator/ReduceMean.cpp
@@ -0,0 +1,61 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include "aidge/operator/ReduceMean.hpp"
+
+#include <algorithm>  // std::for_each, std::sort
+#include <cstddef>    // std::size_t
+#include <cstdint>    // std::int32_t
+#include <memory>
+#include <stdexcept>  // std::runtime_error
+#include <string>
+#include <vector>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
+
+const std::string Aidge::ReduceMean_Op::Type = "ReduceMean";
+
+void Aidge::ReduceMean_Op::computeOutputDims() {
+        if (!getInput(0)) {
+            AIDGE_THROW_OR_ABORT(std::runtime_error, "Every input should be associated with a Tensor");
+        }
+        if (!getInput(0)->empty()) {
+            // make Axes attribute positive
+            std::vector<std::int32_t>& axes = this->template getAttr<ReduceMeanAttr::Axes>();
+            std::for_each(axes.begin(), axes.end(), [&] (std::int32_t& val) {
+                if (val < 0)
+                    val+=static_cast<std::int32_t>(getInput(0)->nbDims());
+            });
+            std::sort(axes.begin(), axes.end());
+
+            // build output dimensions
+            std::vector<DimSize_t> outDims = getInput(0)->dims();
+            if (this->template getAttr<ReduceMeanAttr::KeepDims>()) {
+                std::for_each(axes.cbegin(), axes.cend(), [&outDims] (const std::int32_t& val) { outDims[val] = 1; });
+            }
+            else {
+                for (auto it = axes.crbegin(); it != axes.crend(); ++it)
+                    outDims.erase(outDims.begin() + static_cast<std::size_t>(*it));
+            }
+
+            // TODO: change {1} for {} when scalar Tensors are better handled.
+            mOutputs[0]->resize((outDims.size()>0) ? outDims : std::vector<DimSize_t>({1}));
+
+        }
+    }
+
+void Aidge::ReduceMean_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    SET_IMPL_MACRO(ReduceMean_Op, *this, name);
+    mOutputs[0]->setBackend(name, device);
+}
\ No newline at end of file
diff --git a/src/operator/Reshape.cpp b/src/operator/Reshape.cpp
index c1a7c35e395418995a720efd49c7cfce0801863e..79cfc0659849248bac791ba5b1db25096824e928 100644
--- a/src/operator/Reshape.cpp
+++ b/src/operator/Reshape.cpp
@@ -9,14 +9,18 @@
  *
  ********************************************************************************/
 
+#include "aidge/operator/Reshape.hpp"
+
 #include <cstddef>    // std::size_t
 #include <cstdint>    // std::int64_t
+#include <memory>
 #include <stdexcept>  // std::runtime_error
 #include <string>
 #include <vector>
 
-#include "aidge/operator/Reshape.hpp"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Registrar.hpp"
 #include "aidge/utils/Types.h"
 
 const std::string Aidge::Reshape_Op::Type = "Reshape";
@@ -27,31 +31,37 @@ void Aidge::Reshape_Op::computeOutputDims() {
         AIDGE_THROW_OR_ABORT(std::runtime_error, "Input was not connected");
     }
 
-    std::vector<DimSize_t> outDims;
-
-    // variables to handle a negative dimension
-    bool foundNegativeDimension = false;
-    std::size_t outSize = 1;
-    DimIdx_t negativeIndex = 0;
+    if (!getInput(0)->empty()) {
+        std::vector<DimSize_t> outDims;
+        // variables to handle a negative dimension
+        bool foundNegativeDimension = false;
+        std::size_t outSize = 1;
+        DimIdx_t negativeIndex = 0;
 
-    for(std::size_t i = 0; i < this->template getAttr<ReshapeAttr::Shape>().size(); ++i)
-    {
-        std::int64_t dimSize = this->template getAttr<ReshapeAttr::Shape>()[i];
-        if (dimSize < 0) {
-            if (foundNegativeDimension) {
-                AIDGE_THROW_OR_ABORT(std::runtime_error, "Found more than one negative dimension in Reshape Operator.");
+        for(std::size_t i = 0; i < this->template getAttr<ReshapeAttr::Shape>().size(); ++i)
+        {
+            std::int64_t dimSize = this->template getAttr<ReshapeAttr::Shape>()[i];
+            if (dimSize < 0) {
+                if (foundNegativeDimension) {
+                    AIDGE_THROW_OR_ABORT(std::runtime_error, "Found more than one negative dimension in Reshape Operator.");
+                }
+                foundNegativeDimension = true;
+                dimSize = 1;
+                negativeIndex = static_cast<DimIdx_t>(i);
             }
-            foundNegativeDimension = true;
-            dimSize = 1;
-            negativeIndex = static_cast<DimIdx_t>(i);
+            outDims.push_back(static_cast<DimSize_t>(dimSize));
+            outSize *= static_cast<DimSize_t>(dimSize);
+        }
+
+        if (foundNegativeDimension) {
+            outDims[negativeIndex] = (getInput(0) -> size()) / outSize;
         }
-        outDims.push_back(static_cast<DimSize_t>(dimSize));
-        outSize *= static_cast<DimSize_t>(dimSize);
-    }
 
-    if (foundNegativeDimension) {
-        outDims[negativeIndex] = (getInput(0) -> size()) / outSize;
+        mOutputs[0]->resize(outDims);
     }
+}
 
-    mOutputs[0]->resize(outDims);
+void Aidge::Reshape_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    SET_IMPL_MACRO(Reshape_Op, *this, name);
+    mOutputs[0]->setBackend(name, device);
 }
\ No newline at end of file
diff --git a/src/operator/Scaling.cpp b/src/operator/Scaling.cpp
index 4c121e1268c1e1a62f793f38c6d816e7c6b48c25..8b0d6f9db698e36d232dec38fd8cdd0fad5f8c59 100644
--- a/src/operator/Scaling.cpp
+++ b/src/operator/Scaling.cpp
@@ -9,8 +9,18 @@
  *
  ********************************************************************************/
 
+#include "aidge/operator/Scaling.hpp"
+
+#include <memory>
 #include <string>
 
-#include "aidge/operator/Scaling.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
+
+const std::string Aidge::Scaling_Op::Type = "Scaling";
 
-const std::string Aidge::Scaling_Op::Type = "Scaling";
\ No newline at end of file
+void Aidge::Scaling_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    mImpl = Registrar<Scaling_Op>::create(name)(*this);
+    mOutputs[0]->setBackend(name, device);
+}
\ No newline at end of file
diff --git a/src/operator/Sigmoid.cpp b/src/operator/Sigmoid.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a6edcf823695f95253d6c56e45975480909679d3
--- /dev/null
+++ b/src/operator/Sigmoid.cpp
@@ -0,0 +1,26 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include "aidge/operator/Sigmoid.hpp"
+
+#include <memory>
+#include <string>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
+
+const std::string Aidge::Sigmoid_Op::Type = "Sigmoid";
+
+void Aidge::Sigmoid_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    mImpl = Registrar<Sigmoid_Op>::create(name)(*this);
+    mOutputs[0]->setBackend(name, device);
+}
\ No newline at end of file
diff --git a/src/operator/Slice.cpp b/src/operator/Slice.cpp
index 139e84b561a48c2f6a5ecd14ed9d6905d66dec20..6d2670695b2ffe9acbf09edd3e82f8549a4184f0 100644
--- a/src/operator/Slice.cpp
+++ b/src/operator/Slice.cpp
@@ -27,24 +27,26 @@ const std::string Aidge::Slice_Op::Type = "Slice";
 void Aidge::Slice_Op::computeOutputDims() {
     // check input have been associated
     if (!getInput(0) || (getInput(0)->empty())) {
-        AIDGE_THROW_OR_ABORT(std::runtime_error, "Every input should be associated with a Tensor");
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "{}: input #0 should be associated with a Tensor", type());
     }
 
-    DimSize_t nbAxes = this->template getAttr<SliceAttr::Axes>().size();
+    const DimSize_t nbAxes = this->template getAttr<SliceAttr::Axes>().size();
     std::vector<DimSize_t> outDims = getInput(0)->dims();
     for (std::size_t i = 0; i < nbAxes; ++i) {
         // For each slice operation get the params and cast them to size_t
         const std::int64_t axis_ = this->template getAttr<SliceAttr::Axes>()[i];
         const std::int64_t start_ = this->template getAttr<SliceAttr::Starts>()[i];
         const std::int64_t end_ = this->template getAttr<SliceAttr::Ends>()[i];
-        const std::size_t axis = axis_ >= 0 ? static_cast<std::size_t>(axis_) : axis_ + getInput(0)->nbDims();
-        const std::size_t start = start_ >= 0 ? static_cast<std::size_t>(start_) : start_ + getInput(0)->dims()[axis];
-        const std::size_t end = end_ >= 0 ? static_cast<std::size_t>(end_) : end_ + getInput(0)->dims()[axis];
+        const std::size_t axis = axis_ >= 0 ? static_cast<std::size_t>(axis_) : static_cast<std::size_t>(axis_) + getInput(0)->nbDims();
+        const std::size_t start = start_ >= 0 ? static_cast<std::size_t>(start_) : static_cast<std::size_t>(start_) + getInput(0)->dims()[axis];
+        const std::size_t end = end_ >= 0 ? static_cast<std::size_t>(end_) : static_cast<std::size_t>(end_) + getInput(0)->dims()[axis];
 
         const std::size_t sliceLength = end - start + 1;
         // Check if slice length is valid
         if (sliceLength > getInput(0)->dims()[axis])
+        {
             AIDGE_THROW_OR_ABORT(std::runtime_error, "ROI of Slice operator out of bounds");
+        }
         outDims[axis] = sliceLength;
     }
     mOutputs[0]->resize(outDims);
diff --git a/src/operator/Softmax.cpp b/src/operator/Softmax.cpp
index e88ff4bb4ec6e2cb1357d578c2d07cc4edcb59f7..612c61b0f66b97eb4630214538a22154a67b80d8 100644
--- a/src/operator/Softmax.cpp
+++ b/src/operator/Softmax.cpp
@@ -9,8 +9,18 @@
  *
  ********************************************************************************/
 
+#include "aidge/operator/Softmax.hpp"
+
+#include <memory>
 #include <string>
 
-#include "aidge/operator/Softmax.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
+
+const std::string Aidge::Softmax_Op::Type = "Softmax";
 
-const std::string Aidge::Softmax_Op::Type = "Softmax";
\ No newline at end of file
+void Aidge::Softmax_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    mImpl = Registrar<Softmax_Op>::create(name)(*this);
+    mOutputs[0]->setBackend(name, device);
+}
\ No newline at end of file
diff --git a/src/operator/Sqrt.cpp b/src/operator/Sqrt.cpp
index dbcaba42619762f8fd00bb2f6e0aa0de11d92960..d8ac8b8b0bf28110bd52493d7833f64e9e80fc6a 100644
--- a/src/operator/Sqrt.cpp
+++ b/src/operator/Sqrt.cpp
@@ -9,8 +9,18 @@
  *
  ********************************************************************************/
 
+#include "aidge/operator/Sqrt.hpp"
+
+#include <memory>
 #include <string>
 
-#include "aidge/operator/Sqrt.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
+
+const std::string Aidge::Sqrt_Op::Type = "Sqrt";
 
-const std::string Aidge::Sqrt_Op::Type = "Sqrt";
\ No newline at end of file
+void Aidge::Sqrt_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    mImpl = Registrar<Sqrt_Op>::create(name)(*this);
+    mOutputs[0]->setBackend(name, device);
+}
\ No newline at end of file
diff --git a/src/operator/Sub.cpp b/src/operator/Sub.cpp
index 639eaf798c1c2a9a6685e8b8d2c4a2cb00a4b57a..0c12e6a1fdb7f3b1056e19bf694996d0061b5b04 100644
--- a/src/operator/Sub.cpp
+++ b/src/operator/Sub.cpp
@@ -9,15 +9,18 @@
  *
  ********************************************************************************/
 
-#include <cassert>
-#include <cstddef>
+#include "aidge/operator/Sub.hpp"
+
+#include <cstddef>    // std::size_t
+#include <stdexcept>  // std::runtime_error
+#include <string>
 #include <vector>
-#include <utility>
 
 #include "aidge/backend/OperatorImpl.hpp"
-#include "aidge/operator/Sub.hpp"
-#include "aidge/utils/Types.h"
+#include "aidge/data/Tensor.hpp"
 #include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
 
 const std::string Aidge::Sub_Op::Type = "Sub";
 
@@ -27,11 +30,32 @@ void Aidge::Sub_Op::computeOutputDims() {
         AIDGE_THROW_OR_ABORT(std::runtime_error, "At least one input was not connected");
     }
 
-    if ((!getInput(0)->empty()) &&
-        ((getInput(1)->size() == 1) || // sub by a single value
-        (getInput(1)->size() == getInput(0)->size()) || // sub elem-wise
-        (getInput(1)->nbDims() == 1 && getInput(1)->size() == getInput(0)->dims()[getInput(0)->nbDims()-1]))) // sub by a Tensor with one dimension of output size
-    {
-        mOutputs[0]->resize(getInput(0)->dims());
+    if (!getInput(0)->empty() && !getInput(1)->empty()) {
+
+        const std::vector<std::size_t>& inputsDims0 = getInput(0)->dims();
+        const std::vector<std::size_t>& inputsDims1 = getInput(1)->dims();
+
+        std::vector<std::size_t> outDims = (inputsDims0.size() >= inputsDims1.size()) ? inputsDims0 : inputsDims1;
+        const std::vector<std::size_t>& lowDims = (inputsDims0.size() < inputsDims1.size()) ? inputsDims0 : inputsDims1;
+
+        std::size_t out_id = outDims.size() - 1;
+        std::size_t low_id = lowDims.size() - 1;
+        std::size_t i = 0;
+        while (i++ < lowDims.size()) {
+            if (outDims[out_id] == 1) {
+                outDims[out_id] = lowDims[low_id];
+            }
+            else if ((lowDims[low_id] != 1) && (lowDims[low_id] != outDims[out_id])) {
+                AIDGE_THROW_OR_ABORT(std::runtime_error, "Unsopported Tensor shape for Div Operation");
+            }
+            --out_id;
+            --low_id;
+        }
+        mOutputs[0]->resize(outDims);
     }
-}
\ No newline at end of file
+}
+
+void Aidge::Sub_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    SET_IMPL_MACRO(Sub_Op, *this, name);
+    mOutputs[0]->setBackend(name, device);
+}
diff --git a/src/operator/Tanh.cpp b/src/operator/Tanh.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c113ee6f2da52f40a66a8df04ca33ec4b85f3387
--- /dev/null
+++ b/src/operator/Tanh.cpp
@@ -0,0 +1,26 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include "aidge/operator/Tanh.hpp"
+
+#include <memory>
+#include <string>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/Registrar.hpp"
+#include "aidge/utils/Types.h"
+
+const std::string Aidge::Tanh_Op::Type = "Tanh";
+
+void Aidge::Tanh_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) {
+    mImpl = Registrar<Tanh_Op>::create(name)(*this);
+    mOutputs[0]->setBackend(name, device);
+}
\ No newline at end of file
diff --git a/src/recipes/ConstantFolding.cpp b/src/recipes/ConstantFolding.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..42fb45224614ca2655165a69b974cfe229e27f90
--- /dev/null
+++ b/src/recipes/ConstantFolding.cpp
@@ -0,0 +1,86 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+#include <cassert>
+#include <memory>
+#include <set>
+#include <string>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/graph/GraphView.hpp"
+#include "aidge/graph/Node.hpp"
+#include "aidge/operator/Producer.hpp"
+#include "aidge/recipes/Recipes.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/utils/Types.h"
+
+void Aidge::constantFolding(std::shared_ptr<GraphView> graph) {
+    bool folded;
+    do {
+        folded = false;
+        std::set<std::shared_ptr<Node>> candidates;
+        for (const std::shared_ptr<Node>& nodePtr : graph->getNodes()) {
+            if (nodePtr->type() == Producer_Op::Type) {
+                const auto& childs = nodePtr->getChildren();
+                candidates.insert(childs.begin(), childs.end());
+            }
+        }
+
+        for (const auto& node : candidates) {
+            bool foldable = true;
+            auto replaceGraph = std::make_shared<GraphView>();
+            for (const auto& input : node->inputs()) {
+                if (input.first) {
+                    if (input.first->type() != Producer_Op::Type) {
+                        foldable = false;
+                        break;
+                    }
+
+                    const auto& producer = std::static_pointer_cast<Producer_Op>(input.first->getOperator());
+                    if (!producer->getAttr<bool>("Constant")) {
+                        Log::info("Node {} (of type {}) not foldable because Producer input {} not Constant",
+                            node->name(), node->type(), input.first->name());
+                        foldable = false;
+                        break;
+                    }
+
+                    replaceGraph->add(input.first, false);
+                }
+            }
+
+            if (foldable) {
+                Log::info("Folding node {} (of type {})", node->name(), node->type());
+                replaceGraph->add(node, false);
+
+                node->forward();
+
+                auto prodGraph = std::make_shared<GraphView>();
+                const auto op = std::static_pointer_cast<OperatorTensor>(node->getOperator());
+
+                for (IOIndex_t output = 0; output < node->nbOutputs(); ++output) {
+                    const auto computedOutput = std::make_shared<Tensor>(op->getOutput(output)->clone());
+                    const auto newProd = Producer(computedOutput, node->name() + "_" + std::to_string(output), true);
+
+                    // Add output in right order
+                    prodGraph->add(newProd);
+                }
+
+                if (GraphView::replace(replaceGraph, prodGraph)) {
+                    folded = true;
+                }
+                else {
+                    Log::warn("Error with replace when folding node {} (of type {})",
+                        node->name(), node->type());
+                }
+            }
+        }
+    }
+    while (folded);
+}
diff --git a/src/recipes/ExpandMetaOps.cpp b/src/recipes/ExpandMetaOps.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..16f0b4c52f394e32e24fa49951c39a7c2cb35162
--- /dev/null
+++ b/src/recipes/ExpandMetaOps.cpp
@@ -0,0 +1,36 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <memory>
+
+#include "aidge/recipes/Recipes.hpp"
+#include "aidge/operator/MetaOperator.hpp"
+
+void Aidge::expandMetaOps(std::shared_ptr<GraphView> graph, bool recursive) {
+    bool found = false;
+    const auto nodes = graph->getNodes();
+    for (auto node : nodes) {
+        auto metaOp = std::dynamic_pointer_cast<MetaOperator_Op>(node->getOperator());
+
+        if (metaOp != nullptr) {
+            // Replace meta op by its micro-graph
+            // graph will be updated accordingly in GraphView::replace()
+            auto g = std::make_shared<GraphView>();
+            g->add(node, false);
+            GraphView::replace(g, metaOp->getMicroGraph());
+            found = true;
+        }
+    }
+
+    if (found && recursive) {
+        expandMetaOps(graph, true);
+    }
+}
diff --git a/src/recipies/ExplicitCastMove.cpp b/src/recipes/ExplicitCastMove.cpp
similarity index 95%
rename from src/recipies/ExplicitCastMove.cpp
rename to src/recipes/ExplicitCastMove.cpp
index 5651f2ba4cc939678ab306137464c52caa1db46c..7d836c3acc835c5ed3fe014db6787029dc318afd 100644
--- a/src/recipies/ExplicitCastMove.cpp
+++ b/src/recipes/ExplicitCastMove.cpp
@@ -9,7 +9,7 @@
  *
  ********************************************************************************/
 
-#include "aidge/recipies/Recipies.hpp"
+#include "aidge/recipes/Recipes.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
 #include "aidge/operator/Cast.hpp"
 #include "aidge/operator/Move.hpp"
@@ -20,6 +20,7 @@ void Aidge::explicitCastMove(std::shared_ptr<GraphView> graph) {
     for (auto node : nodes) {
         // TODO: currently, Operator data type is only reflected in its output tensor data type.
         // But an Operator might have multiple outputs of different data type(?)
+        AIDGE_ASSERT(node->getOperator()->operatorType() == OperatorType::Tensor, "Operator must be of Tensor type.");
         const auto& output = std::static_pointer_cast<OperatorTensor>(node->getOperator())->getOutput(0);
         if (output->getImpl() == nullptr) {
             continue;
@@ -32,6 +33,7 @@ void Aidge::explicitCastMove(std::shared_ptr<GraphView> graph) {
             const auto parent = node->inputs()[0];
             // Check parent is not nullptr, as this Operator may be an entry point of the graph without parent
             if (parent.first != nullptr) {
+                AIDGE_ASSERT(parent.first->getOperator()->operatorType() == OperatorType::Tensor, "Operator must be of Tensor type.");
                 const auto& input = std::static_pointer_cast<OperatorTensor>(parent.first->getOperator())->getOutput(parent.second);
 
                 if ((node->type() == Cast_Op::Type && input->dataType() == output->dataType())
diff --git a/src/recipies/FuseBatchNorm.cpp b/src/recipes/FuseBatchNorm.cpp
similarity index 93%
rename from src/recipies/FuseBatchNorm.cpp
rename to src/recipes/FuseBatchNorm.cpp
index 2fb017567550ada083d0d79d0323b0b45998026f..76c15a0627ee65ed23c2dc385d9cd3787f9f0979 100644
--- a/src/recipies/FuseBatchNorm.cpp
+++ b/src/recipes/FuseBatchNorm.cpp
@@ -21,7 +21,7 @@
 #include "aidge/operator/ConvDepthWise.hpp"
 #include "aidge/operator/FC.hpp"
 #include "aidge/operator/MetaOperator.hpp"
-#include "aidge/recipies/Recipies.hpp"
+#include "aidge/recipes/Recipes.hpp"
 #include "aidge/utils/ErrorHandling.hpp"
 #include "aidge/utils/Types.h"
 
@@ -50,9 +50,10 @@ void Aidge::fuseBatchNorm(std::shared_ptr<Aidge::Node> convNode,
     const std::shared_ptr<BatchNorm_Op<2>> batchOp =
             std::static_pointer_cast<BatchNorm_Op<2>>(batchnormNode->getOperator());
 
-    DimSize_t convNbOutChannels;
-    DimSize_t channelsSize;
-    std::array<DimSize_t, 2> kernelDims;
+    DimSize_t convNbOutChannels = 1;
+    DimSize_t channelsSize = 1;
+    std::array<DimSize_t, 2> kernelDims = {1,1};
+    AIDGE_ASSERT(convNode->getOperator()->operatorType() == OperatorType::Tensor, "Operator must be of Tensor type.");
     std::shared_ptr<OperatorTensor> convOp = std::static_pointer_cast<OperatorTensor>(convNode->getOperator());
     if (convNode->type() == Conv_Op<2>::Type) {
         const std::shared_ptr<Conv_Op<2>> convOpPtr =
@@ -65,7 +66,6 @@ void Aidge::fuseBatchNorm(std::shared_ptr<Aidge::Node> convNode,
         const std::shared_ptr<ConvDepthWise_Op<2>> convOpPtr =
             std::static_pointer_cast<ConvDepthWise_Op<2>>(convNode->getOperator());
         convNbOutChannels = convOpPtr->getAttr<DimSize_t>("Channels");
-        channelsSize = 1;
         kernelDims = convOpPtr->getAttr<std::array<DimSize_t, 2>>("KernelDims");
     }
 
@@ -89,13 +89,13 @@ void Aidge::fuseBatchNorm(std::shared_ptr<Aidge::Node> convNode,
             meanVariance += b_var.get<float>(outChId);
             ++count;
         } else {
-            printf("Zero-variance: %s [%lu]\n", convNode->name().c_str(), outChId);
+            fmt::print("Zero-variance: {} [{}]\n", convNode->name(), outChId);
         }
     }
     if (count > 0)
         meanVariance /= count;
     else {
-        printf("Warning: variance < 1e-12 for all outputs! Is the network correctly trained?\n");
+        fmt::print("Warning: variance < 1e-12 for all outputs! Is the network correctly trained?\n");
     }
 
     std::shared_ptr<Tensor> weightBuf, biasBuf;
@@ -172,7 +172,7 @@ void Aidge::fuseBatchNorm(std::shared_ptr<Aidge::MatchSolution> solution) {
 void Aidge::fuseBatchNorm(std::shared_ptr<Aidge::GraphView> graphView) {
     std::shared_ptr<GraphRegex> regex = std::make_shared<GraphRegex>();
     regex->setNodeKey("BatchNorm", "getType($) =='BatchNorm'");
-    printf("\n============================\nSearching for solutions\n==============================\n");
+    fmt::print("\n============================\nSearching for solutions\n==============================\n");
     regex->setNodeKey(
             "OP",
             "getType($) =='Conv' || getType($) =='ConvDepthWise' || getType($) =='PaddedConv' || getType($) =='PaddedConvDepthWise'");
diff --git a/src/recipies/FuseMulAdd.cpp b/src/recipes/FuseMulAdd.cpp
similarity index 50%
rename from src/recipies/FuseMulAdd.cpp
rename to src/recipes/FuseMulAdd.cpp
index 322b1d9a0632b893a912c6225ac5b13d63278f8d..6582038e981bb58534d04ded57052f6a0f83e9a9 100644
--- a/src/recipies/FuseMulAdd.cpp
+++ b/src/recipes/FuseMulAdd.cpp
@@ -15,7 +15,7 @@
 #include <string>
 
 #include "aidge/operator/FC.hpp"
-#include "aidge/recipies/Recipies.hpp"
+#include "aidge/recipes/Recipes.hpp"
 #include "aidge/graph/GraphView.hpp"
 #include "aidge/graph/Node.hpp"
 #include "aidge/operator/Producer.hpp"
@@ -36,12 +36,53 @@ void Aidge::fuseMulAdd(std::shared_ptr<Aidge::Node> matmulNode, std::shared_ptr<
 
     // Step 1 : Create FC
     // Fetch the output dimension throught the bias size
-    std::shared_ptr<Node> bias = (addNode->getParent(1)) ? addNode->getParent(1)->cloneSharedOperators() : nullptr;
-
-    AIDGE_ASSERT(matmulNode->getParent(1), "No weight detected to produce the fuseMulAdd recipe.");
+    std::shared_ptr<Node> bias = nullptr;
+    if (addNode->getParent(0) == matmulNode) {
+        AIDGE_ASSERT(matmulNode->getParent(1), "No bias detected to produce the fuseMulAdd recipe.");
+        bias = addNode->getParent(1);
+    }
+    else if (addNode->getParent(1) == matmulNode) {
+        AIDGE_ASSERT(matmulNode->getParent(0), "No bias detected to produce the fuseMulAdd recipe.");
+        bias = addNode->getParent(0);
+    }
 
-    std::shared_ptr<Node> weight = matmulNode->getParent(1)->cloneSharedOperators();
-    const DimSize_t outSize = std::dynamic_pointer_cast<MatMul_Op>(matmulNode->getOperator()) -> getAttr<DimSize_t>("OutChannels");
+    std::shared_ptr<Node> weight = nullptr;
+    if ((matmulNode->getParent(1) && !matmulNode->getParent(0))
+        || (matmulNode->getParent(1) && matmulNode->getParent(1)->getOperator()->type() == Producer_Op::Type
+            && matmulNode->getParent(0) && matmulNode->getParent(0)->getOperator()->type() != Producer_Op::Type))
+    {
+        weight = matmulNode->getParent(1);
+    }
+    else if ((matmulNode->getParent(0) && !matmulNode->getParent(1))
+        || (matmulNode->getParent(0) && matmulNode->getParent(0)->getOperator()->type() == Producer_Op::Type
+            && matmulNode->getParent(1) && matmulNode->getParent(1)->getOperator()->type() != Producer_Op::Type))
+    {
+        weight = matmulNode->getParent(0);
+    }
+    else if (matmulNode->getParent(0) && matmulNode->getParent(0)->getOperator()->type() == Producer_Op::Type
+        && matmulNode->getParent(1) && matmulNode->getParent(1)->getOperator()->type() == Producer_Op::Type)
+    {
+        // If both inputs are producers, there is an ambiguity, but both options
+        // result in a correct solution.
+        Log::notice("Notice: both MatMul inputs are Producers, assume data at input#0 and weights at input#1.");
+        weight = matmulNode->getParent(1);
+    }
+    AIDGE_ASSERT(weight != nullptr, "Could not deduce weight input for MatMul operator.");
+
+    // TODO: find another way to get OutChannels for FC operator.
+    // This poor fix supposes that one of Add inputs is a const and has the same outChannels as the output
+    DimSize_t outSize = 0;
+    AIDGE_ASSERT(addNode->getOperator()->operatorType() == OperatorType::Tensor, "Operator must be of Tensor type.");
+    const auto& op = std::static_pointer_cast<OperatorTensor>(addNode->getOperator());
+    for (size_t i = 0; i < op->nbInputs(); i++)
+    {
+        const auto& inTensor = op->getInput(i);
+        if(inTensor->nbDims() > 0) {
+            outSize = inTensor->dims()[inTensor->nbDims()-1];
+            break;
+        }
+    }
+    AIDGE_ASSERT(outSize, "Couldnt get output number of channels for FC operator.");
 
     // Instanciate FC
     //std::shared_ptr<Node> fc = FC(dim[0], false, "Fc");
@@ -49,9 +90,9 @@ void Aidge::fuseMulAdd(std::shared_ptr<Aidge::Node> matmulNode, std::shared_ptr<
 
     // Step 2 : Branch existing producers & create the others
     // link weights & bias
-    weight->addChild(fc, 0, 1);
+    weight->cloneSharedOperators()->addChild(fc, 0, 1);
     if (bias) {
-        bias->addChild(fc, 0, 2);
+        bias->cloneSharedOperators()->addChild(fc, 0, 2);
     }
 
 
@@ -59,8 +100,8 @@ void Aidge::fuseMulAdd(std::shared_ptr<Aidge::Node> matmulNode, std::shared_ptr<
         // Case 1 : If all nodes are in a graph view : delete old nodes & branch input & output
         // Case 2 : If not all nodes are in a graph view : only delete the nodes from the graphview
         // Maybe create a central mechanism to update automatically all graph views rather than each node have graphview presence memory?
-    auto newNodes = std::set<std::shared_ptr<Node>>({fc, weight, fc->getParent(2)});
-    GraphView::replace({matmulNode, addNode, addNode->getParent(1), matmulNode->getParent(1)}, newNodes);
+    auto newNodes = std::set<std::shared_ptr<Node>>({fc, fc->getParent(1), fc->getParent(2)});
+    GraphView::replace({matmulNode, addNode, bias, weight}, newNodes);
 
 }
 
diff --git a/src/recipes/GraphViewHelper.cpp b/src/recipes/GraphViewHelper.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3b42db7fe18d2269b95cf35fd92851d1e3684bad
--- /dev/null
+++ b/src/recipes/GraphViewHelper.cpp
@@ -0,0 +1,57 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <memory>
+#include <set>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/graph/Node.hpp"
+#include "aidge/graph/GraphView.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+#include "aidge/recipes/GraphViewHelper.hpp"
+
+
+std::set<std::shared_ptr<Aidge::Tensor>> Aidge::producers(std::shared_ptr<Aidge::GraphView> graphview) {
+    std::set<std::shared_ptr<Tensor>> res;
+    const auto& nodes = graphview->getNodes();
+    for (const auto& node : nodes) {
+        if (node->type() == "Producer") {
+            const auto& param = std::static_pointer_cast<OperatorTensor>(node->getOperator());
+            res.insert(param->getOutput(0));
+        }
+    }
+    return res;
+}
+
+
+std::set<std::shared_ptr<Aidge::Tensor>> Aidge::parameters(std::shared_ptr<Aidge::GraphView> graphview) {
+    std::set<std::shared_ptr<Tensor>> res;
+    const auto& nodes = graphview->getNodes();
+    for (const auto& node : nodes) {
+        const auto& param = std::static_pointer_cast<OperatorTensor>(node->getOperator());
+        for (std::size_t o = 0; o < param->nbOutputs(); ++o) {
+            res.insert(param->getOutput(o));
+        }
+    }
+    return res;
+}
+
+void Aidge::compile_gradient(std::shared_ptr<Aidge::GraphView> gv) {
+    for (const auto& node : gv->getNodes()) {
+        // TODO: check that each node is an OperatorTensor
+        AIDGE_ASSERT(node->getOperator()->operatorType() == OperatorType::Tensor, "Cannot instanciate gradient of an Operator ({}) that doesn't use Tensor.", node->getOperator()->type());
+        const std::shared_ptr<OperatorTensor> op = std::dynamic_pointer_cast<OperatorTensor>(node -> getOperator());
+        for (std::size_t o = 0; o < node -> nbOutputs(); ++o) {
+            op->getOutput(o)->initGradient();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/recipies/HorizontalTiling.cpp b/src/recipes/HorizontalTiling.cpp
similarity index 87%
rename from src/recipies/HorizontalTiling.cpp
rename to src/recipes/HorizontalTiling.cpp
index 6cc34eba076934b884b336ce40081a855d917182..8e27fea58014b4ec16729f3593dd656026e16826 100644
--- a/src/recipies/HorizontalTiling.cpp
+++ b/src/recipes/HorizontalTiling.cpp
@@ -15,7 +15,7 @@
 #include <vector>
 #include <utility>
 
-#include "aidge/recipies/Recipies.hpp"
+#include "aidge/recipes/Recipes.hpp"
 
 #include "aidge/graph/Node.hpp"
 #include "aidge/graph/GraphView.hpp"
@@ -36,7 +36,8 @@ std::set<std::shared_ptr<Aidge::Node>> Aidge::getConvHorizontalTiling(const std:
     if (node->getOperator()->type() != "Conv") {
         AIDGE_INTERNAL_ASSERT("Operator should be a Convolution.");
     }
-    const auto& op = std::dynamic_pointer_cast<OperatorTensor>(node->getOperator());
+    AIDGE_ASSERT(node->getOperator()->operatorType() == OperatorType::Tensor, "Operator must be of Tensor type.");
+    const auto& op = std::static_pointer_cast<OperatorTensor>(node->getOperator());
     if (op->nbOutputs() != 1 || op->nbData() > 1) {
         AIDGE_INTERNAL_ASSERT("Only slice Operators with one output and at most one input for now.");
     }
@@ -82,16 +83,16 @@ std::set<std::shared_ptr<Aidge::Node>> Aidge::getConvHorizontalTiling(const std:
         clonedInputs[1] -> addChild(newNode, 0, 1);
         clonedInputs[2] -> addChild(newNode, 0, 2);
         // Slice for input and each parameter
-        std::vector<std::int32_t> inputDimsEnd(inputDims[0].first.size());
+        std::vector<std::int64_t> inputDimsEnd(inputDims[0].first.size());
         for (std::size_t dim = 0; dim < inputDimsEnd.size(); ++dim) {
-            inputDimsEnd[dim] = static_cast<std::int32_t>(inputDims[0].first[dim] + inputDims[0].second[dim]) - 1;
+            inputDimsEnd[dim] = static_cast<std::int64_t>(inputDims[0].first[dim] + inputDims[0].second[dim]) - 1;
         }
-        std::vector<std::int32_t> inputDimsStart(inputDims[0].first.size());
+        std::vector<std::int64_t> inputDimsStart(inputDims[0].first.size());
         for (std::size_t dim = 0; dim < inputDimsStart.size(); ++dim) {
-            inputDimsStart[dim] = static_cast<std::int32_t>(inputDims[0].first[dim]);
+            inputDimsStart[dim] = static_cast<std::int64_t>(inputDims[0].first[dim]);
         }
-        std::vector<std::int32_t> usedDims(inputDimsEnd.size());
-        std::iota(usedDims.begin(), usedDims.end(), static_cast<std::int32_t>(0));
+        std::vector<std::int64_t> usedDims(inputDimsEnd.size());
+        std::iota(usedDims.begin(), usedDims.end(), static_cast<std::int64_t>(0));
         auto slice = Slice(inputDimsStart, inputDimsEnd, usedDims, "Slice_" + std::to_string(currentFirstDims[axis]));
         slice -> addChild(newNode, 0, 0);
         newNode -> addChild(concat, 0, i);
diff --git a/src/recipies/LabelGraph.cpp b/src/recipes/LabelGraph.cpp
similarity index 98%
rename from src/recipies/LabelGraph.cpp
rename to src/recipes/LabelGraph.cpp
index 6966bb81d000b62d904f800233048fa58998c6fb..ac0e6bfe197460c8c422a6c1f3b3240518ee1f29 100644
--- a/src/recipies/LabelGraph.cpp
+++ b/src/recipes/LabelGraph.cpp
@@ -11,7 +11,7 @@
 
 #include <memory>
 
-#include "aidge/recipies/LabelGraph.hpp"
+#include "aidge/recipes/LabelGraph.hpp"
 #include "aidge/operator/Conv.hpp"
 #include "aidge/operator/ConvDepthWise.hpp"
 #include "aidge/operator/AvgPooling.hpp"
diff --git a/src/recipies/RemoveDropout.cpp b/src/recipes/RemoveDropout.cpp
similarity index 96%
rename from src/recipies/RemoveDropout.cpp
rename to src/recipes/RemoveDropout.cpp
index 1dedac8f19e6ec6b4b1f6dabb6bd3e9b8c759def..4f8805845bd1f46fd187cba3564b031c55c4655a 100644
--- a/src/recipies/RemoveDropout.cpp
+++ b/src/recipes/RemoveDropout.cpp
@@ -10,11 +10,10 @@
  ********************************************************************************/
 
 #include <memory>
-#include <iostream>
 
 #include "aidge/graph/Node.hpp"
 #include "aidge/graph/GraphView.hpp"
-#include "aidge/recipies/Recipies.hpp"
+#include "aidge/recipes/Recipes.hpp"
 
 //Graph Regex
 #include "aidge/graphRegex/GraphRegex.hpp"
diff --git a/src/recipies/RemoveFlatten.cpp b/src/recipes/RemoveFlatten.cpp
similarity index 97%
rename from src/recipies/RemoveFlatten.cpp
rename to src/recipes/RemoveFlatten.cpp
index d571b53023b7665c25aedc869628045b3b13d509..8c1bf1bcf0bf79fda275867ff6430d5a937da172 100644
--- a/src/recipies/RemoveFlatten.cpp
+++ b/src/recipes/RemoveFlatten.cpp
@@ -13,7 +13,7 @@
 
 #include "aidge/graph/Node.hpp"
 #include "aidge/graph/GraphView.hpp"
-#include "aidge/recipies/Recipies.hpp"
+#include "aidge/recipes/Recipes.hpp"
 
 
 //Graph Regex
@@ -22,7 +22,6 @@
 
 namespace Aidge {
     void removeFlatten(std::shared_ptr<Node> flatten) {
- 
         GraphView::replace({flatten}, {});
     }
 
diff --git a/src/scheduler/MemoryManager.cpp b/src/scheduler/MemoryManager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6fe0d1f0745a464b8fd61bf634d7105b9d22faf8
--- /dev/null
+++ b/src/scheduler/MemoryManager.cpp
@@ -0,0 +1,916 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <fmt/format.h>
+
+#include "aidge/scheduler/MemoryManager.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+
+Aidge::MemoryManager::~MemoryManager() noexcept = default;
+
+std::shared_ptr<Aidge::MemoryManager::MemorySpace> Aidge::MemoryManager::reserve(
+    unsigned int size,
+    const std::set<std::shared_ptr<Node> >& dependencies)
+{
+    const unsigned int offset = onStack(size);
+
+    std::shared_ptr<MemorySpace> memSpace
+        = std::make_shared<MemorySpace>(mClock, offset, size, dependencies);
+    mMemSpaces.push_back(memSpace);
+    return memSpace;
+}
+
+void Aidge::MemoryManager::expand(
+    std::shared_ptr<MemorySpace> memSpace,
+    unsigned int requiredSize)
+{
+    assert(std::find(mMemSpaces.begin(), mMemSpaces.end(), memSpace)
+            != mMemSpaces.end());
+
+    memSpace->size = std::max(memSpace->size, requiredSize);
+
+    // Rebuild the stack from the beginning, taking into account the new size.
+    // Everything else stay the same.
+    mMemStack.clear();
+
+    for (Clock_T clock = 0; clock <= mClock; ++clock) {
+        for (std::vector<std::shared_ptr<MemorySpace> >::iterator
+            it = mMemSpaces.begin(), itEnd = mMemSpaces.end(); it != itEnd;
+            ++it)
+        {
+            if ((*it)->allocated == clock)
+                (*it)->offset = onStack((*it)->size);
+        }
+
+        // MemorySpace released at clock are still valid until the next tick;
+        // make sure offStack() only append after all onStack() are done.
+        for (std::vector<std::shared_ptr<MemorySpace> >::iterator
+            it = mMemSpaces.begin(), itEnd = mMemSpaces.end(); it != itEnd;
+            ++it)
+        {
+            if ((*it)->released == clock && (*it)->dependencies.empty())
+                offStack((*it)->offset);
+        }
+    }
+}
+
+Aidge::MemoryManager::MemoryPlane Aidge::MemoryManager::allocate(
+    unsigned int size,
+    const std::set<std::shared_ptr<Node> >& dependencies,
+    unsigned int stride,
+    unsigned int length,
+    unsigned int count)
+{
+    const unsigned int fullSize = std::max(size, stride) * length * count;
+    return MemoryPlane(reserve(fullSize, dependencies),
+                       mClock, 0, size, stride, length, count);
+}
+
+unsigned int Aidge::MemoryManager::allocate(
+    const std::shared_ptr<Node>& node,
+    unsigned int size,
+    const std::set<std::shared_ptr<Node> >& dependencies,
+    unsigned int stride,
+    unsigned int length,
+    unsigned int count)
+{
+    std::map<std::shared_ptr<Node>, std::vector<MemoryPlane> >::iterator it;
+    std::tie(it, std::ignore) = mMemPlanes.insert(std::make_pair(node,
+                                                std::vector<MemoryPlane>()));
+
+    (*it).second.push_back(allocate(size, dependencies, stride, length, count));
+    return ((*it).second.size() - 1);
+}
+
+bool Aidge::MemoryManager::isWrapAround(
+    std::shared_ptr<MemorySpace> memSpace,
+    unsigned int offset,
+    unsigned int size,
+    unsigned int stride,
+    unsigned int length,
+    unsigned int count) const
+{
+    const unsigned int fullSize = std::max(size, stride) * length * count;
+    return (offset + fullSize > memSpace->size);
+}
+
+Aidge::MemoryManager::MemoryPlane Aidge::MemoryManager::reallocate(
+    std::shared_ptr<MemorySpace> memSpace,
+    unsigned int offset,
+    unsigned int size,
+    bool wrapAround,
+    unsigned int extraSize,
+    const std::set<std::shared_ptr<Node> >& additionalDependencies,
+    unsigned int stride,
+    unsigned int length,
+    unsigned int count)
+{
+    const unsigned int fullSize = std::max(size, stride) * length * count;
+    unsigned int requiredSize = offset + fullSize;
+
+    if (wrapAround) {
+        requiredSize = fullSize + extraSize;
+
+        if (count > 1) {
+            // (requiredSize - offset) must be a multiple of (stride * length)
+            requiredSize = offset
+                + std::ceil((requiredSize - offset)
+                    / static_cast<double>(std::max(size, stride) * length))
+                        * (std::max(size, stride) * length);
+        }
+        else if (length > 1) {
+            // (requiredSize - offset) must be a multiple of stride
+            requiredSize = offset
+                + std::ceil((requiredSize - offset)
+                    / static_cast<double>(std::max(size, stride)))
+                        * std::max(size, stride);
+        }
+    }
+
+    if (requiredSize > memSpace->size || memSpace->released >= 0) {
+        // Expand in size and/or duration.
+        // If memSpace was already released, put it back on the stack
+        memSpace->released = -1;
+        expand(memSpace, requiredSize);
+    }
+
+    memSpace->dependencies.insert(additionalDependencies.begin(),
+                                  additionalDependencies.end());
+
+    return MemoryPlane(memSpace, mClock, offset, size, stride, length, count);
+}
+
+Aidge::MemoryManager::MemoryPlane Aidge::MemoryManager::reallocate(
+    const MemoryPlane& memPlane,
+    unsigned int extraOffset,
+    unsigned int size,
+    bool wrapAround,
+    unsigned int extraSize,
+    const std::set<std::shared_ptr<Node> >& additionalDependencies,
+    unsigned int stride,
+    unsigned int length,
+    unsigned int count)
+{
+    const unsigned int initialOffset = memPlane.getFinalOffset()
+        - memPlane.memSpace->offset + extraOffset;
+    const unsigned int fullSize = std::max(size, stride) * length * count;
+    unsigned int requiredSize = initialOffset + fullSize;
+
+    if (wrapAround) {
+        requiredSize = fullSize + extraSize;
+
+        if (count > 1) {
+            // (requiredSize - offset) must be a multiple of (stride * length)
+            requiredSize = initialOffset
+                + std::ceil((requiredSize - initialOffset)
+                    / static_cast<double>(std::max(size, stride) * length))
+                        * (std::max(size, stride) * length);
+        }
+        else if (length > 1) {
+            // (requiredSize - offset) must be a multiple of stride
+            requiredSize = initialOffset
+                + std::ceil((requiredSize - initialOffset)
+                    / static_cast<double>(std::max(size, stride)))
+                        * std::max(size, stride);
+        }
+
+        // Make sure that the intended margin with previous memPlane will be
+        // respected, as it may actually be lower because of the floor()
+        // in the memPlane getLimit() function.
+        if (memPlane.count > 1) {
+            requiredSize = memPlane.offset
+                + std::ceil((requiredSize - memPlane.offset)
+                    / static_cast<double>(memPlane.stride * memPlane.length))
+                        * (memPlane.stride * memPlane.length);
+        }
+        else if (memPlane.length > 1) {
+            requiredSize = memPlane.offset
+                + std::ceil((requiredSize - memPlane.offset)
+                    / static_cast<double>(memPlane.stride))
+                        * memPlane.stride;
+        }
+    }
+
+    if (requiredSize > memPlane.memSpace->size
+        || memPlane.memSpace->released >= 0)
+    {
+        // Expand in size and/or duration.
+        // If memSpace was already released, put it back on the stack
+        memPlane.memSpace->released = -1;
+        expand(memPlane.memSpace, requiredSize);
+    }
+
+    memPlane.memSpace->dependencies.insert(
+        additionalDependencies.begin(),
+        additionalDependencies.end());
+
+    const unsigned int finalOffset = memPlane.getFinalOffset()
+        - memPlane.memSpace->offset + extraOffset;
+
+    return MemoryPlane(memPlane.memSpace, mClock,
+                       finalOffset, size, stride, length, count);
+}
+
+unsigned int Aidge::MemoryManager::reallocate(
+    const MemoryPlane& memPlane,
+    const std::shared_ptr<Node>& node,
+    unsigned int extraOffset,
+    unsigned int size,
+    bool wrapAround,
+    unsigned int extraSize,
+    const std::set<std::shared_ptr<Node> >& additionalDependencies,
+    unsigned int stride,
+    unsigned int length,
+    unsigned int count)
+{
+    std::map<std::shared_ptr<Node>, std::vector<MemoryPlane> >::iterator it;
+    std::tie(it, std::ignore) = mMemPlanes.insert(std::make_pair(node,
+                                                std::vector<MemoryPlane>()));
+
+    (*it).second.push_back(reallocate(memPlane, extraOffset, size, wrapAround,
+                                      extraSize, additionalDependencies,
+                                      stride, length, count));
+    return ((*it).second.size() - 1);
+}
+
+unsigned int Aidge::MemoryManager::reallocate(
+    std::shared_ptr<MemorySpace> memSpace,
+    const std::shared_ptr<Node>& node,
+    unsigned int offset,
+    unsigned int size,
+    bool wrapAround,
+    unsigned int extraSize,
+    const std::set<std::shared_ptr<Node> >& additionalDependencies,
+    unsigned int stride,
+    unsigned int length,
+    unsigned int count)
+{
+    std::map<std::shared_ptr<Node>, std::vector<MemoryPlane> >::iterator it;
+    std::tie(it, std::ignore) = mMemPlanes.insert(std::make_pair(node,
+                                                std::vector<MemoryPlane>()));
+
+    (*it).second.push_back(reallocate(memSpace, offset, size, wrapAround,
+                                      extraSize, additionalDependencies,
+                                      stride, length, count));
+    return ((*it).second.size() - 1);
+}
+
+unsigned int Aidge::MemoryManager::release(std::shared_ptr<MemorySpace> memSpace)
+{
+    if (memSpace->released == -1) {
+        memSpace->released = mClock;
+
+        if (memSpace->dependencies.empty())
+            return offStack(memSpace->offset);
+    }
+
+    return 0;
+}
+
+unsigned int Aidge::MemoryManager::release(const std::shared_ptr<Node>& node)
+{
+    const std::map<std::shared_ptr<Node>, std::vector<MemoryPlane> >
+        ::iterator it = mMemPlanes.find(node);
+
+    if (it == mMemPlanes.end()) {
+        fmt::print("Warning: release(): there is no allocated memory for node {}\n", node->name());
+        return 0;
+    }
+
+    unsigned int releasedMemSize = 0;
+
+    for (std::vector<MemoryPlane>::iterator itPlanes = (*it).second.begin(),
+        itPlanesEnd = (*it).second.end(); itPlanes != itPlanesEnd; ++itPlanes)
+    {
+        releasedMemSize += release((*itPlanes).memSpace);
+    }
+
+    // Remove dependencies
+    releasedMemSize += releaseDependencies(node);
+
+    return releasedMemSize;
+}
+
+unsigned int Aidge::MemoryManager::releaseDependencies(
+    const std::shared_ptr<Node>& node)
+{
+    unsigned int releasedMemSize = 0;
+
+    for (std::vector<std::shared_ptr<MemorySpace> >::iterator
+        it = mMemSpaces.begin(), itEnd = mMemSpaces.end(); it != itEnd;
+        ++it)
+    {
+        if (!(*it)->dependencies.empty()) {
+            (*it)->dependencies.erase(node);
+
+            if ((*it)->released <= mClock
+                && (*it)->dependencies.empty())
+            {
+                (*it)->released = mClock;
+                releasedMemSize += offStack((*it)->offset);
+            }
+        }
+    }
+
+    return releasedMemSize;
+}
+
+bool Aidge::MemoryManager::MaxLifetimeMinSizeFirst::operator()(
+    const std::shared_ptr<MemorySpace>& p0,
+    const std::shared_ptr<MemorySpace>& p1)
+{
+    const Clock_T lifetime0
+        = ((p0->released >= 0) ? p0->released : maxLifetime) - p0->allocated;
+    const Clock_T lifetime1
+        = ((p1->released >= 0) ? p1->released : maxLifetime) - p1->allocated;
+
+    return (lifetime0 > lifetime1
+            || (lifetime0 == lifetime1 && p0->size < p1->size));
+}
+
+bool Aidge::MemoryManager::MaxLifetimeMaxSizeFirst::operator()(
+    const std::shared_ptr<MemorySpace>& p0,
+    const std::shared_ptr<MemorySpace>& p1)
+{
+    const Clock_T lifetime0
+        = ((p0->released >= 0) ? p0->released : maxLifetime) - p0->allocated;
+    const Clock_T lifetime1
+        = ((p1->released >= 0) ? p1->released : maxLifetime) - p1->allocated;
+
+    return (lifetime0 > lifetime1
+            || (lifetime0 == lifetime1 && p0->size > p1->size));
+}
+
+bool Aidge::MemoryManager::MaxHoleMaxLifetimeFirst::operator()(
+    const std::shared_ptr<MemorySpace>& p0,
+    const std::shared_ptr<MemorySpace>& p1)
+{
+    const Clock_T lifetime0
+        = ((p0->released >= 0) ? p0->released : maxLifetime) - p0->allocated;
+    const Clock_T lifetime1
+        = ((p1->released >= 0) ? p1->released : maxLifetime) - p1->allocated;
+
+    const std::pair<Clock_T, unsigned int> maxHole0 = inst->getMaxHole(p0);
+    const std::pair<Clock_T, unsigned int> maxHole1 = inst->getMaxHole(p1);
+
+    return (maxHole0.second > maxHole1.second
+            || (maxHole0.second == maxHole1.second && lifetime0 > lifetime1));
+}
+
+void Aidge::MemoryManager::optimize(OptimizeStrategy strategy) {
+    if (strategy == None)
+        return;
+
+    const unsigned int maxLifetime = getMaxLifetime();
+
+    if (strategy == OptimizeMaxLifetimeMinSizeFirst) {
+        std::stable_sort(mMemSpaces.begin(), mMemSpaces.end(),
+                        MemoryManager::MaxLifetimeMinSizeFirst(maxLifetime));
+    }
+    else if (strategy == OptimizeMaxLifetimeMaxSizeFirst) {
+        std::stable_sort(mMemSpaces.begin(), mMemSpaces.end(),
+                        MemoryManager::MaxLifetimeMaxSizeFirst(maxLifetime));
+    }
+    else if (strategy == OptimizeMaxHoleMaxLifetimeFirst) {
+        std::stable_sort(mMemSpaces.begin(), mMemSpaces.end(),
+                        MemoryManager::MaxHoleMaxLifetimeFirst(maxLifetime, this));
+    }
+
+    std::vector<std::map<unsigned int, unsigned int> > stacks(maxLifetime + 1,
+                                        std::map<unsigned int, unsigned int>());
+
+    for (std::vector<std::shared_ptr<MemorySpace> >::const_iterator
+        it = mMemSpaces.begin(), itEnd = mMemSpaces.end(); it != itEnd; ++it)
+    {
+        const Clock_T maxT = ((*it)->released >= 0
+                                && (*it)->dependencies.empty())
+                                    ? (*it)->released : maxLifetime;
+
+        // Merge stacks over memSpace lifetime
+        std::map<unsigned int, unsigned int> mergedStacks;
+
+        for (Clock_T t = (*it)->allocated; t <= maxT; ++t) {
+            for (std::map<unsigned int, unsigned int>::iterator itMem
+                = stacks[t].begin(), itMemEnd = stacks[t].end();
+                itMem != itMemEnd; ++itMem)
+            {
+                bool newInsert;
+                std::map<unsigned int, unsigned int>::iterator itMergedMem;
+                std::tie(itMergedMem, newInsert) = mergedStacks.insert(
+                    std::make_pair((*itMem).first, (*itMem).second));
+
+                if (!newInsert) {
+                    (*itMergedMem).second = std::max((*itMergedMem).second,
+                                                     (*itMem).second);
+                }
+            }
+        }
+
+        std::map<unsigned int, unsigned int> mergedStack;
+
+        if (!mergedStacks.empty()) {
+            std::map<unsigned int, unsigned int>::iterator itMem
+                = mergedStacks.begin();
+
+            mergedStack.insert(*itMem);
+            ++itMem;
+
+            while (itMem != mergedStacks.end()) {
+                std::map<unsigned int, unsigned int>::reverse_iterator
+                    itMergedMem = mergedStack.rbegin();
+                const unsigned int nextOffset = (*itMergedMem).first
+                                                + (*itMergedMem).second;
+
+                if ((*itMem).first <= nextOffset) {
+                    (*itMergedMem).second
+                        = std::max((*itMem).first + (*itMem).second, nextOffset)
+                            - (*itMergedMem).first;
+                }
+                else
+                    mergedStack.insert(*itMem);
+
+                ++itMem;
+            }
+        }
+
+        // Allocate in merged stack
+        unsigned int offset = 0;
+        std::map<unsigned int, unsigned int>::iterator itMem
+            = mergedStack.begin();
+
+        while (true) {
+            if (itMem == mergedStack.end()
+                || (*itMem).first - offset >= (*it)->size)
+            {
+                mergedStack.insert(std::make_pair(offset, (*it)->size));
+                break;
+            }
+            else {
+                offset = (*itMem).first + (*itMem).second;
+                ++itMem;
+            }
+        }
+
+        (*it)->offset = offset;
+
+        for (Clock_T t = (*it)->allocated; t <= maxT; ++t) {
+            const std::map<unsigned int, unsigned int> stack
+                = getStack((*it), t);
+            stacks[t].insert(stack.begin(), stack.end());
+
+            //stacks[t].insert(std::make_pair(offset, (*it)->size));
+        }
+    }
+}
+
+unsigned int Aidge::MemoryManager::getOffset(const std::shared_ptr<Node>& node,
+                                            unsigned int plane) const
+{
+    const std::map<std::shared_ptr<Node>, std::vector<MemoryPlane> >
+        ::const_iterator it = mMemPlanes.find(node);
+
+    if (it == mMemPlanes.end()) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error,
+            "getOffset(): no memory allocated for node name {}", node->name());
+    }
+
+    if (plane >= (*it).second.size()) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error,
+            "getOffset(): plane out of range for node name {}", node->name());
+    }
+
+    return ((*it).second[plane].memSpace->offset + (*it).second[plane].offset);
+}
+
+unsigned int Aidge::MemoryManager::getSize(const std::shared_ptr<Node>& node,
+                                          unsigned int plane) const
+{
+    const std::map<std::shared_ptr<Node>, std::vector<MemoryPlane> >
+        ::const_iterator it = mMemPlanes.find(node);
+
+    if (it == mMemPlanes.end()) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error,
+            "getSize(): no memory allocated for node name {}", node->name());
+    }
+
+    if (plane >= (*it).second.size()) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error,
+            "getSize(): plane out of range for node name {}", node->name());
+    }
+
+    return (*it).second[plane].getSize();
+}
+
+unsigned int Aidge::MemoryManager::getSize(const std::shared_ptr<Node>& node)
+    const
+{
+    const std::map<std::shared_ptr<Node>, std::vector<MemoryPlane> >
+        ::const_iterator it = mMemPlanes.find(node);
+
+    if (it == mMemPlanes.end()) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error,
+            "getSize(): no memory allocated for node name {}", node->name());
+    }
+
+    unsigned int size = 0;
+
+    for (std::vector<MemoryPlane>::const_iterator itPlanes
+        = (*it).second.begin(), itPlanesEnd = (*it).second.end();
+        itPlanes != itPlanesEnd; ++itPlanes)
+    {
+        size += (*itPlanes).getSize();
+    }
+
+    return size;
+}
+
+unsigned int Aidge::MemoryManager::getNbPlanes(const std::shared_ptr<Node>& node)
+    const
+{
+    const std::map<std::shared_ptr<Node>, std::vector<MemoryPlane> >
+        ::const_iterator it = mMemPlanes.find(node);
+    return (it == mMemPlanes.end()) ? 0 : (*it).second.size();
+}
+
+unsigned int Aidge::MemoryManager::getPeakUsage() const {
+    unsigned int peakUsage = 0;
+
+    for (std::vector<std::shared_ptr<MemorySpace> >::const_iterator
+        it = mMemSpaces.begin(), itEnd = mMemSpaces.end(); it != itEnd; ++it)
+    {
+        peakUsage = std::max(peakUsage,
+                             (*it)->offset + (*it)->size);
+    }
+
+    return peakUsage;
+}
+
+Aidge::MemoryManager::Clock_T Aidge::MemoryManager::getMaxLifetime() const {
+    Clock_T maxLifetime = 0;
+
+    for (std::vector<std::shared_ptr<MemorySpace> >::const_iterator
+        it = mMemSpaces.begin(), itEnd = mMemSpaces.end(); it != itEnd; ++it)
+    {
+        maxLifetime = std::max(maxLifetime,
+            std::max((*it)->allocated, (*it)->released));
+    }
+
+    return maxLifetime;
+}
+
+const std::vector<Aidge::MemoryManager::MemoryPlane>&
+Aidge::MemoryManager::getPlanes(const std::shared_ptr<Node>& node) const
+{
+    const std::map<std::shared_ptr<Node>, std::vector<MemoryPlane> >
+        ::const_iterator it = mMemPlanes.find(node);
+
+    if (it == mMemPlanes.end()) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error,
+            "getSize(): no memory allocated for node name {}", node->name());
+    }
+
+    return (*it).second;
+}
+
+Aidge::MemoryManager::MemMap_T
+Aidge::MemoryManager::getPlanes(std::shared_ptr<MemorySpace> memSpace)
+    const
+{
+    MemMap_T planes;
+
+    for (MemMap_T::const_iterator itNode = mMemPlanes.begin(),
+        itNodeEnd = mMemPlanes.end(); itNode != itNodeEnd; ++itNode)
+    {
+        for (std::vector<MemoryPlane>::const_iterator itPlane
+             = (*itNode).second.begin(), itPlaneEnd = (*itNode).second.end();
+             itPlane != itPlaneEnd; ++itPlane)
+        {
+            if ((*itPlane).memSpace == memSpace) {
+                std::map<std::shared_ptr<Node>, std::vector<MemoryPlane> >
+                    ::iterator it;
+                std::tie(it, std::ignore) = planes.insert(
+                    std::make_pair((*itNode).first,
+                                   std::vector<MemoryPlane>()));
+
+                (*it).second.push_back((*itPlane));
+            }
+        }
+    }
+
+    return planes;
+}
+
+unsigned int Aidge::MemoryManager::getNbPlanes(
+    std::shared_ptr<MemorySpace> memSpace) const
+{
+    unsigned int count = 0;
+
+    for (std::map<std::shared_ptr<Node>, std::vector<MemoryPlane> >
+        ::const_iterator itNode = mMemPlanes.begin(),
+        itNodeEnd = mMemPlanes.end(); itNode != itNodeEnd; ++itNode)
+    {
+        for (std::vector<MemoryPlane>::const_iterator itPlane
+             = (*itNode).second.begin(), itPlaneEnd = (*itNode).second.end();
+             itPlane != itPlaneEnd; ++itPlane)
+        {
+            if ((*itPlane).memSpace == memSpace)
+                ++count;
+        }
+    }
+
+    return count;
+}
+
+void Aidge::MemoryManager::tick()
+{
+    ++mClock;
+}
+
+void Aidge::MemoryManager::log(const std::string& fileName) const
+{
+    auto memData = std::unique_ptr<FILE, decltype(&std::fclose)>(std::fopen(fileName.c_str(), "w"), &std::fclose);
+
+    if (!memData) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error,
+            "Could not create memory layout log file: {}", fileName);
+    }
+
+    auto gnuplot = std::unique_ptr<FILE, decltype(&std::fclose)>(std::fopen((fileName + "_plot.gnu").c_str(), "w"), &std::fclose);
+
+    if (!gnuplot) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error,
+            "Could not create memory layout log file: {}", (fileName + "_plot.gnu"));
+    }
+
+    const Clock_T maxLifetime = getMaxLifetime();
+    const unsigned int peakUsage = getPeakUsage();
+
+    fmt::print(gnuplot.get(), "#!/usr/bin/gnuplot\n");
+    fmt::print(gnuplot.get(), "set term pngcairo size 1280,768 noenhanced\n");
+    fmt::print(gnuplot.get(), "set output \"{}\"\n", fileName + "_plot.png");
+    fmt::print(gnuplot.get(), "set xrange [{}:{}]\n", 0, maxLifetime + 1);
+    fmt::print(gnuplot.get(), "set yrange [{}:{}]\n", 0, 1.05 * (peakUsage / 1024.0));
+    fmt::print(gnuplot.get(), "set xlabel \"Time\"\n");
+    fmt::print(gnuplot.get(), "set ylabel \"Memory usage (KWords)\"\n");
+    fmt::print(gnuplot.get(), "set grid\n");
+    fmt::print(gnuplot.get(), "set xtics 1\n");
+    fmt::print(gnuplot.get(), "unset key\n");
+    fmt::print(gnuplot.get(), "set palette rgbformulae 30,31,32\n");
+    fmt::print(gnuplot.get(), "unset colorbox\n");
+    fmt::print(gnuplot.get(), "N={}\n", mMemPlanes.size() + 1);
+
+    unsigned int objectId = 1;
+    unsigned int labelId = 1;
+
+    for (std::map<std::shared_ptr<Node>, std::vector<MemoryPlane> >
+        ::const_iterator it = mMemPlanes.begin(), itEnd = mMemPlanes.end();
+        it != itEnd; ++it)
+    {
+        const std::string name = (*it).first->name();
+        fmt::print(memData.get(), "{}\n", name);
+
+        double minX = -1;
+        unsigned int maxY = 0;
+
+        for (std::vector<MemoryPlane>::const_iterator itPlanes
+             = (*it).second.begin(), itPlanesBegin = (*it).second.begin(),
+            itPlanesEnd = (*it).second.end(); itPlanes != itPlanesEnd;
+            ++itPlanes)
+        {
+            const unsigned int contiguousOffset
+                = (*itPlanes).getContiguousOffset();
+            const unsigned int contiguousSize = (*itPlanes).getContiguousSize();
+            const unsigned int wrappedOffset = (*itPlanes).getWrappedOffset();
+            const unsigned int wrappedSize = (*itPlanes).getWrappedSize();
+
+            const Clock_T allocated = (*itPlanes).allocated;
+            const Clock_T released = (*itPlanes).memSpace->released;
+            const bool isReleased = (released >= 0
+                                && (*itPlanes).memSpace->dependencies.empty());
+
+            fmt::print(memData.get(), "  {} {} ({:#08x}U) -> {} ({:#08x}U)",
+                (itPlanes - itPlanesBegin), contiguousOffset, contiguousOffset,
+                (contiguousOffset + contiguousSize), (contiguousOffset + contiguousSize));
+
+            if (wrappedSize > 0) {
+                fmt::print(memData.get(), " + {} ({:#08x}U) -> {} ({:#08x}U)",
+                    wrappedOffset, wrappedOffset,
+                    (wrappedOffset + wrappedSize), (wrappedOffset + wrappedSize));
+            }
+
+            fmt::print(memData.get(), " [{}] @ {}", (*itPlanes).getSize(), allocated);
+
+            if (isReleased) {
+                fmt::print(memData.get(), " to {}", released);
+            }
+
+            fmt::print(memData.get(), "\n");
+
+            // Gnuplot
+            const double startX = allocated;
+
+            if (startX < minX || minX < 0) {
+                minX = startX;
+                maxY = contiguousOffset + contiguousSize;
+            }
+
+            if ((*itPlanes).size != (*itPlanes).stride) {
+                for (unsigned int offset = contiguousOffset;
+                    offset < contiguousOffset + contiguousSize;
+                    offset += (*itPlanes).stride)
+                {
+                    fmt::print(gnuplot.get(), "set object {} rectangle from {},{} to {},{} fc palette frac ({} * 1./N)\n",
+                        (allocated * 100 + objectId), startX, (offset / 1024.0),
+                        (((isReleased) ? released : maxLifetime) + 1),
+                        (std::min((offset + (*itPlanes).size),
+                                        contiguousOffset + contiguousSize) / 1024.0),
+                        labelId);
+                    ++objectId;
+                }
+            }
+            else {
+                fmt::print(gnuplot.get(), "set object {} rectangle from {},{} to {},{} fc palette frac ({} * 1./N)\n",
+                    (allocated * 100 + objectId), startX, (contiguousOffset / 1024.0),
+                    (((isReleased) ? released : maxLifetime) + 1),
+                    ((contiguousOffset + contiguousSize) / 1024.0),
+                    labelId);
+                ++objectId;
+            }
+
+            if (wrappedSize > 0) {
+                fmt::print(gnuplot.get(), "set object {} rectangle from {},{} to {},{} fc palette frac ({} * 1./N)\n",
+                    (allocated * 100 + objectId), startX, (wrappedOffset / 1024.0),
+                    (((isReleased) ? released : maxLifetime) + 1),
+                    ((wrappedOffset + contiguousSize) / 1024.0),
+                    labelId);
+                ++objectId;
+
+                fmt::print(gnuplot.get(), "set arrow from {},{} to {},{} nohead\n",
+                    startX, (contiguousOffset / 1024.0),
+                    (startX + 0.1), (contiguousOffset / 1024.0));
+
+                fmt::print(gnuplot.get(), "set arrow from {},{} to {},{} nohead\n",
+                    (startX + 0.05), ((contiguousOffset + contiguousSize) / 1024.0),
+                    (startX + 0.05), (wrappedOffset / 1024.0));
+            }
+        }
+
+        fmt::print(gnuplot.get(), "set label {} '{}' at {},{} rotate by 30 font \",8\" offset char 0.5,0.5\n",
+            labelId, name, minX, (maxY / 1024.0));
+        ++labelId;
+
+        fmt::print(memData.get(), "\n");
+    }
+
+    fmt::print(gnuplot.get(), "set arrow from 0,{} to {},{} nohead lc rgb \"red\"\n",
+        (peakUsage / 1024.0), (maxLifetime + 1),
+        (peakUsage / 1024.0));
+
+    fmt::print(gnuplot.get(), "set label {} 'Peak usage = {} KWords' at 0,{} textcolor rgb \"red\" offset char 0.5,0.5\n",
+        labelId, (peakUsage / 1024.0), (peakUsage / 1024.0));
+
+    fmt::print(gnuplot.get(), "plot 0\n");
+}
+
+unsigned int Aidge::MemoryManager::onStack(unsigned int size)
+{
+    unsigned int offset = 0;
+    std::map<unsigned int, unsigned int>::iterator itMem = mMemStack.begin();
+
+    while (true) {
+        if (itMem == mMemStack.end()
+            || (*itMem).first - offset >= size)
+        {
+            mMemStack.insert(std::make_pair(offset, size));
+            break;
+        }
+        else {
+            offset = (*itMem).first + (*itMem).second;
+            ++itMem;
+        }
+    }
+
+    return offset;
+}
+
+unsigned int Aidge::MemoryManager::offStack(unsigned int offset)
+{
+    std::map<unsigned int, unsigned int>::iterator itMem
+        = mMemStack.find(offset);
+
+    if (itMem == mMemStack.end()) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error,
+            "offStack(): offset not found in stack");
+    }
+    else {
+        const unsigned int size = (*itMem).second;
+        mMemStack.erase(offset);
+        return size;
+    }
+}
+
+std::map<unsigned int, unsigned int> Aidge::MemoryManager::getStack(
+    std::shared_ptr<MemorySpace> memSpace,
+    Clock_T clock) const
+{
+    // Find all planes associated to memSpace and index them by their allocated
+    // value in a map
+    std::map<Clock_T, std::vector<MemoryPlane> > planes;
+
+    for (std::map<std::shared_ptr<Node>, std::vector<MemoryPlane> >
+        ::const_iterator itNode = mMemPlanes.begin(),
+        itNodeEnd = mMemPlanes.end(); itNode != itNodeEnd; ++itNode)
+    {
+        for (std::vector<MemoryPlane>::const_iterator itPlane
+             = (*itNode).second.begin(), itPlaneEnd = (*itNode).second.end();
+             itPlane != itPlaneEnd; ++itPlane)
+        {
+            if ((*itPlane).memSpace == memSpace) {
+                std::map<Clock_T, std::vector<MemoryPlane> >::iterator it;
+                std::tie(it, std::ignore) = planes.insert(
+                    std::make_pair((*itPlane).allocated,
+                                   std::vector<MemoryPlane>()));
+
+                (*it).second.push_back((*itPlane));
+            }
+        }
+    }
+
+    // Find the planes allocated at time clock or the one just before
+    // => obtain all the planes that are considered valid at the time clock
+    Clock_T c = clock;
+    std::map<Clock_T, std::vector<MemoryPlane> >::iterator itPlanes;
+
+    do
+        itPlanes = planes.find(c);
+    while (itPlanes == planes.end() && (c--) > 0);
+
+    assert(itPlanes != planes.end());
+
+    // Fill the stack at time clock
+    std::map<unsigned int, unsigned int> stack;
+
+    for (std::vector<MemoryPlane>::const_iterator
+        it = (*itPlanes).second.begin(), itEnd = (*itPlanes).second.end();
+        it != itEnd; ++it)
+    {
+        stack.insert(std::make_pair((*it).getContiguousOffset(),
+                                    (*it).getContiguousSize()));
+
+        if ((*it).getWrappedSize() > 0) {
+            stack.insert(std::make_pair((*it).getWrappedOffset(),
+                                        (*it).getWrappedSize()));
+        }
+    }
+
+    return stack;
+}
+
+std::pair<Aidge::MemoryManager::Clock_T, unsigned int>
+Aidge::MemoryManager::getMaxHole(std::shared_ptr<MemorySpace> memSpace) const
+{
+    std::map<Clock_T, unsigned int> holesSize;
+
+    for (std::map<std::shared_ptr<Node>, std::vector<MemoryPlane> >
+        ::const_iterator itNode = mMemPlanes.begin(),
+        itNodeEnd = mMemPlanes.end(); itNode != itNodeEnd; ++itNode)
+    {
+        for (std::vector<MemoryPlane>::const_iterator itPlane
+             = (*itNode).second.begin(), itPlaneEnd = (*itNode).second.end();
+             itPlane != itPlaneEnd; ++itPlane)
+        {
+            if ((*itPlane).memSpace == memSpace) {
+                const unsigned int holeSize = memSpace->size
+                    - (*itPlane).getContiguousSize()
+                    - (*itPlane).getWrappedSize();
+
+                std::map<Clock_T, unsigned int>::iterator it;
+                bool newInsert;
+                std::tie(it, newInsert) = holesSize.insert(
+                    std::make_pair((*itPlane).allocated, holeSize));
+
+                if (!newInsert) {
+                    // Another plane exists at the same time, one must substract
+                    // the size of this other plane from the hole size
+                    (*it).second = std::max(0, static_cast<int>((*it).second)
+                        - static_cast<int>((*itPlane).getContiguousSize())
+                        - static_cast<int>((*itPlane).getWrappedSize()));
+                }
+            }
+        }
+    }
+
+    return *std::max_element(holesSize.begin(),
+                             holesSize.end(),
+                             [](const auto& left, const auto& right) {
+                                return std::max(left.second, right.second);
+                             });
+}
diff --git a/src/scheduler/ParallelScheduler.cpp b/src/scheduler/ParallelScheduler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1dd13fe2100122002d4ed068ada4851b1bfba463
--- /dev/null
+++ b/src/scheduler/ParallelScheduler.cpp
@@ -0,0 +1,200 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include "aidge/scheduler/ParallelScheduler.hpp"
+#include "aidge/scheduler/ThreadPool.hpp"
+
+#include <chrono>
+#include <memory>
+#include <set>
+#include <string>
+
+#include <fmt/ranges.h>
+#include <fmt/color.h>
+
+#include "aidge/graph/GraphView.hpp"
+#include "aidge/graph/Node.hpp"
+#include "aidge/utils/Types.h"
+#include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/operator/Producer.hpp"
+#include "aidge/operator/Memorize.hpp"
+#include "aidge/operator/MetaOperator.hpp"
+
+void Aidge::ParallelScheduler::forward(bool forwardDims, std::vector<std::shared_ptr<Aidge::Tensor>> data) {
+    // Collect all data input of the graph (that are producers)
+    if (!data.empty()){
+        connectInputs(data);
+    }
+
+    // Forward dims (if allowed)
+    if (forwardDims) {mGraphView->forwardDims(); }
+
+    // Generate scheduling *only if empty*
+    // If scheduling was already generated (in one or several steps, i.e. one or
+    // several successive call to generateScheduling()), do not generate it twice
+    if (mStaticSchedule.empty()) {
+        this->generateScheduling();
+    }
+
+    const auto namePtrTable = mGraphView->getRankedNodesName("{0} ({1}#{3})");
+
+    // Sort static scheduling, the order will be the prefered threads scheduling
+    // order for non critical nodes
+    std::deque<std::shared_ptr<StaticSchedulingElement>> staticSchedule(mStaticSchedule.at(mStaticScheduleStep).begin(), mStaticSchedule.at(mStaticScheduleStep).end());
+    std::stable_sort(staticSchedule.begin(), staticSchedule.end(),
+        [](const auto& lhs, const auto& rhs) { return ((lhs->early < rhs->early) || (lhs->early == rhs->early && lhs->late < rhs->late)); });
+
+    // The thread pool has N threads running to process nodes.
+    // Thread pooling avoid the overhead of threads creation and deletion for
+    // each node execution.
+    ThreadPool pool;
+
+    size_t latest = 0;
+    std::mutex schedulingMutex;
+    std::map<std::shared_ptr<StaticSchedulingElement>, std::atomic<bool>> finished;
+
+    while (!staticSchedule.empty()) {
+        Log::debug("Step {}", latest);
+
+        std::vector<std::shared_ptr<StaticSchedulingElement>> mustFinish;
+
+        // Run all nodes that must be run at this step: latest (critical nodes)
+        for (size_t i = 0; i < staticSchedule.size(); ) {
+            auto runnable = staticSchedule[i];
+
+            if (runnable->late == latest) {
+                // Wait for potential preceding non-critical nodes to finish
+                while (true) {
+                    bool ready = true;
+                    for (auto elt : runnable->laterThan) {
+                        ready = ready && finished.at(elt);
+                    }
+                    if (!ready) {
+                        std::this_thread::yield();
+                    }
+                    else {
+                        break;
+                    }
+                }
+
+                // Add the critical node to the thread pool queue, to be run ASAP
+                finished[runnable] = false;
+                pool.queueJob([node = runnable->node, &finished = finished.at(runnable), &schedulingMutex, this]() {
+                    const auto tStart = std::chrono::high_resolution_clock::now();
+                    node->forward();
+                    const auto tEnd = std::chrono::high_resolution_clock::now();
+                    finished = true;
+                    {
+                        std::unique_lock<std::mutex> lock(schedulingMutex);
+                        mScheduling.emplace_back(SchedulingElement(node, tStart, tEnd));
+                    }
+                });
+                staticSchedule.erase(staticSchedule.begin() + i);
+                mustFinish.push_back(runnable);
+
+                Log::debug("  run critical {}", namePtrTable.at(runnable->node));
+
+                // Ensure the following nodes cannot start earlier than next step
+                for (auto elt : runnable->earlierThan) {
+                    if (elt->early < latest + 1) {
+                        Log::debug("    {}: {} -> {}", namePtrTable.at(elt->node), elt->early, latest + 1);
+                        elt->early = latest + 1;
+                        AIDGE_INTERNAL_ASSERT(elt->early <= elt->late);
+                    }
+                }
+            }
+            else if (runnable->early > latest + 1) {
+                // There cannot be more node that must be run at latest + 1
+                // latest + 1 and not latest because early may have been updated
+                // for some elements to latest + 1 (above).
+                break;
+            }
+            else {
+                ++i;
+            }
+        }
+
+        // If some threads are still available, run next early nodes
+        // These nodes are non-critical, meaning they can still be run at least
+        // in the next step
+        for (size_t i = 0; i < staticSchedule.size(); ) {
+            auto runnable = staticSchedule[i];
+            if (!pool.busy() && runnable->early <= latest) {
+                // Check that potential preceding non-critical nodes are finished
+                bool ready = true;
+                for (auto elt : runnable->laterThan) {
+                    ready = ready && finished.at(elt);
+                }
+
+                if (ready) {
+                    // All preceding nodes have finished, this node can be run.
+                    // Add the node to the thread pool queue, to be run ASAP
+                    finished[runnable] = false;
+                    pool.queueJob([node = runnable->node, &finished = finished.at(runnable), &schedulingMutex, this]() {
+                        const auto tStart = std::chrono::high_resolution_clock::now();
+                        node->forward();
+                        const auto tEnd = std::chrono::high_resolution_clock::now();
+                        finished = true;
+                        {
+                            std::unique_lock<std::mutex> lock(schedulingMutex);
+                            mScheduling.emplace_back(SchedulingElement(node, tStart, tEnd));
+                        }
+                    });
+                    staticSchedule.erase(staticSchedule.begin() + i);
+
+                    Log::debug("  run {}", namePtrTable.at(runnable->node));
+
+                    // Ensure the following nodes cannot start earlier than next step
+                    for (auto elt : runnable->earlierThan) {
+                        if (elt->early < latest + 1) {
+                            Log::debug("    {}: {} -> {}", namePtrTable.at(elt->node), elt->early, latest + 1);
+                            elt->early = latest + 1;
+                            AIDGE_INTERNAL_ASSERT(elt->early <= elt->late);
+                        }
+                    }
+                }
+                else {
+                    // The node cannot be run yet, because preceding nodes are
+                    // still running, move to the next one in schedule
+                    ++i;
+                }
+            }
+            else {
+                // Thread pool is already full or no more node can be run at
+                // this step (latest)
+                break;
+            }
+        }
+
+        // Wait for all nodes that must finish at latest to be finished
+        // By scheduling construction, no other node can be started before all 
+        // nodes at latest step are finished
+        while (true) {
+            bool ready = true;
+            for (auto elt : mustFinish) {
+                ready = ready && finished.at(elt);
+            }
+            if (!ready) {
+                std::this_thread::yield();
+            }
+            else {
+                break;
+            }
+        }
+
+        ++latest;
+    }
+
+    ++mStaticScheduleStep;
+    if (mStaticScheduleStep == mStaticSchedule.size()) {
+        mStaticScheduleStep = 0;
+    }
+}
diff --git a/src/scheduler/Scheduler.cpp b/src/scheduler/Scheduler.cpp
index 3afbcd0442fd40214687751d50bfc98809bba840..4e3f9978837120bd01a3de2cfe2d22e33f9d7828 100644
--- a/src/scheduler/Scheduler.cpp
+++ b/src/scheduler/Scheduler.cpp
@@ -11,92 +11,149 @@
 
 #include "aidge/scheduler/Scheduler.hpp"
 
+#include <algorithm> // std::find, std::find_if, std::max, std::min, std::replace, std::transform
+#include <cassert>
 #include <chrono>
+#include <cstddef>   // std::size_t
+#include <cstdio>    // std::fclose, std::fopen
+#include <iterator>  // std::back_inserter, std::distance
+#include <map>
 #include <memory>
 #include <set>
 #include <string>
+#include <vector>
+
+#include <fmt/core.h>
+#include <fmt/color.h>
+#include <fmt/ranges.h>
 
 #include "aidge/graph/GraphView.hpp"
 #include "aidge/graph/Node.hpp"
-#include "aidge/utils/Types.h"
+#include "aidge/operator/Memorize.hpp"
+#include "aidge/operator/MetaOperator.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/operator/Producer.hpp"
+#include "aidge/utils/Types.h"
+
+
+Aidge::Scheduler::~Scheduler() noexcept = default;
+Aidge::Scheduler::PriorProducersConsumers::PriorProducersConsumers() = default;
+Aidge::Scheduler::PriorProducersConsumers::PriorProducersConsumers(const PriorProducersConsumers&) = default;
+Aidge::Scheduler::PriorProducersConsumers::~PriorProducersConsumers() noexcept = default;
 
-void drawProgressBar(double progress, int barWidth, const std::string& additionalInfo = "") {
-    putchar('[');
-    int pos = static_cast<int>(barWidth * progress);
-    for (int i = 0; i < barWidth; ++i) {
-        if (i <= pos)
-            putchar('#');
-        else
-            putchar(' ');
-    }
-    printf("] %d%% | %s\r", static_cast<int>(progress * 100), additionalInfo.c_str());
-    fflush(stdout);
+void Aidge::Scheduler::generateScheduling() {
+    auto schedule = generateBaseScheduling();
+    generateEarlyLateScheduling(schedule);
+    mStaticSchedule.push_back(schedule);
 }
 
-void Aidge::SequentialScheduler::generateScheduling(bool verbose) {
-    // TODO: For loop on the list of node to run
-    // run sequencially every runnable consumers once
-    // TODO: handle memory allocation in scheduler
-    // TODO: optimize memory usage
+std::vector<std::shared_ptr<Aidge::Scheduler::StaticSchedulingElement>> Aidge::Scheduler::generateBaseScheduling() const {
+
+    // 0) setup useful variables
+    // map associating each node with string "name (type#rank)"
+    const std::map<std::shared_ptr<Node>, std::string> namePtrTable
+        = mGraphView->getRankedNodesName("{0} ({1}#{3})");
+
+    // consumers that were run by but can still consume data.
+    // They must be run AFTER the remaining consumer to ensure a non-greedy
+    // producers-consumers model!
+    std::set<std::shared_ptr<Node>> stillConsumers;
+
+    std::vector<std::shared_ptr<StaticSchedulingElement>> schedule;
+
+
+    // 1) Initialize consumers list:
+    // 1.1) List of the GraphView's input nodes
+    std::set<std::shared_ptr<Node>> consumers = mGraphView->inputNodes();
 
-    // setup initial producers list
+    // 1.2) List of nodes inside the GraphView connected to an inner Producer
     std::set<std::shared_ptr<Node>> producers;
     for (const std::shared_ptr<Node>& nodePtr : mGraphView->getNodes()) {
-        if (nodePtr->type() == "Producer") {
-            producers.insert(nodePtr);
+        if (nodePtr->type() == Producer_Op::Type) {
+            for (const auto& child : nodePtr->getChildren()) {
+                // Do not schedule childs outside current graph!
+                if (mGraphView->inView(child)) {
+                    consumers.insert(child);
+                }
+            }
         }
     }
-    // add Data Input
-    // FIXME : should be changed when the real system for providing
-    // data is implemented
-    for (const std::shared_ptr<Node>& nodePtr : mGraphView->inputNodes()) {
-        for (const auto& parentPtr : nodePtr->getParents()) {
-            if ((mGraphView->getNodes()).find(parentPtr) == (mGraphView->getNodes()).end()) {
-                // Node not found in the graph, it's an outside producer
-                producers.insert(parentPtr);
+
+    do {
+        // 2) From the current consumers list, check if any prior consumer node
+        // is needed. A prior will generally be required for any node consuming
+        // parameters (weights and bias) that is not an input node.
+        // If for a given node, only parent producers (at any depth) are needed
+        // to satisfy its required data, it becomes a prior.
+        // If the prior node is a producer, it is added to the list of required
+        // producers.
+        // If the prior node is of another type, it replaces the initial consumer
+        // in the new priorConsumers list. The initial consumer will become
+        // again a consumer later, by construction.
+        Log::debug("List of consumers with their priors:");
+        std::set<std::shared_ptr<Node>> requiredProducers;  // Priors of type Producer
+        std::set<std::shared_ptr<Node>> priorConsumers;  // Priors of other type
+        mPriorCache.clear();
+
+        for (const auto& consumer : consumers) {
+            Log::debug("\t- consumer: {}", fmt::styled(namePtrTable.at(consumer), fg(fmt::color::orange)));
+
+            const auto& prior = getPriorProducersConsumers(consumer);
+
+            if (prior.isPrior) {
+                std::vector<std::string> requiredProducersName;
+                std::transform(prior.requiredProducers.begin(), prior.requiredProducers.end(),
+                    std::back_inserter(requiredProducersName),
+                    [&namePtrTable](auto val){ return namePtrTable.at(val); });
+                Log::debug("\t\trequired producers: {}", requiredProducersName);
+
+                std::vector<std::string> priorConsumersName;
+                std::transform(prior.priorConsumers.begin(), prior.priorConsumers.end(),
+                    std::back_inserter(priorConsumersName),
+                    [&namePtrTable](auto val){ return namePtrTable.at(val); });
+                Log::debug("\t\tprior consumers: {}", priorConsumersName);
+
+                requiredProducers.insert(prior.requiredProducers.cbegin(), prior.requiredProducers.cend());
+                priorConsumers.insert(prior.priorConsumers.cbegin(), prior.priorConsumers.cend());
+            }
+            else {
+                priorConsumers.insert(consumer);
             }
         }
-    }
 
-    // setup consumer list
-    // std::set<std::shared_ptr<Node>> consumers = getConsumers(producers);
+        // 3) Prior consumers replace the initial consumers list.
+        // By construction, initial consumers will necessarily become consumers
+        // again later.
+        consumers.swap(priorConsumers);
 
-    /* It may not be necessary to initialize producer */
-    std::set<std::shared_ptr<Node>> consumers = mGraphView->inputNodes();
-    do {
-        // find runnable consumers
+        // 4) Make producers generate the required data.
+        // Producers are special nodes that generate data on demand.
+        for (const auto& requiredProducer : requiredProducers) {
+            requiredProducer->getOperator()->updateConsummerProducer();
+            schedule.push_back(std::make_shared<StaticSchedulingElement>(requiredProducer));
+        }
+
+        // 5) Find runnable consumers.
+        // A consumer is runnable if the required data is available for all of
+        // its inputs. At this point, not all consumers are necessarily
+        // runnable because some may depend on the execution of others (when
+        // there is multiple successive priors for example).
         std::set<std::shared_ptr<Node>> runnableConsumers;
-        if (verbose) printf("List of layers receiving data:\n");
+        Log::debug("Updated list of consumers:");
         for (const auto& consumer : consumers) {
-            if (verbose) {
-                printf("\t- consumer: "
-                       "\x1b[1;37m"
-                       "%s"
-                       "\x1b[0m"
-                       "\n\t\tR/C:\t",
-                       (consumer->type() + "_" + std::to_string(reinterpret_cast<uintptr_t>(consumer.get()))).c_str());
-                for (IOIndex_t inId = 0; inId < consumer->nbInputs() - 1; ++inId) {
-                    printf("%zu/%zu\n\t\t\t", consumer->getOperator()->getNbConsumedData(inId),
-                           consumer->getOperator()->getNbRequiredData(inId));
-                }
-                printf("%zu/%zu", consumer->getOperator()->getNbConsumedData(static_cast<IOIndex_t>(consumer->nbInputs()) - 1),
-                       consumer->getOperator()->getNbRequiredData(static_cast<IOIndex_t>(consumer->nbInputs()) - 1));
-                printf("\n\t\tP:\t");
-                for (IOIndex_t outId = 0; outId < consumer->nbOutputs() - 1; ++outId) {
-                    printf("%zu\n\t\t\t", consumer->getOperator()->getNbProducedData(outId));
-                }
-                printf("%zu", consumer->getOperator()->getNbProducedData(static_cast<IOIndex_t>(consumer->nbOutputs()) - 1));
-                printf("\n");
-            }
+            summarizeConsumerState(consumer, namePtrTable.at(consumer));  // debug print
+
             bool isRunnable = true;
+            for (IOIndex_t inputIdx = 0; inputIdx < consumer->nbInputs(); ++inputIdx) {
+                AIDGE_LOG_CONTEXT("Consumer node {} input #{}", namePtrTable.at(consumer), inputIdx);
+
+                if ((consumer->getOperator()->getNbConsumedData(inputIdx) + consumer->getOperator()->getNbRequiredData(inputIdx)) >
+                            getNbAvailableData(consumer, inputIdx)) {
+                    Log::debug("  not runnable: C{} + R{} > P{} for input #{}",
+                        consumer->getOperator()->getNbConsumedData(inputIdx),
+                        consumer->getOperator()->getNbRequiredData(inputIdx),
+                        getNbAvailableData(consumer, inputIdx), inputIdx);
 
-            IOIndex_t parentID = 0;  // FIXME: handle this correctly
-            // Check every input has got enought data to run
-            for (const auto& consumerParent : consumer->dataInputs()) {
-                if (consumerParent.first &&
-                    consumer->getOperator()->getNbRequiredData(parentID++) >
-                            consumerParent.first->getOperator()->getNbProducedData(consumerParent.second)) {
                     // not enough data to run
                     isRunnable = false;
                     break;
@@ -108,134 +165,545 @@ void Aidge::SequentialScheduler::generateScheduling(bool verbose) {
             }
         }
 
-        // Push consumers in the list of nodes to run and update the consumer producer system
+        // 5) If not consumer is runnable, it is a stop condition!
+        if (runnableConsumers.empty()) {
+            Log::debug("********************");
+            // No consumer is runnable: some required data is missing for all of
+            // them. There is two possibilities:
+            // - At least one required data source is exhausted, which may be
+            //   an expected stop condition.
+            // - There is a deadlock between consumers, if some one is waiting
+            //   for data from the other and reciprocally.
+            break;
+        }
+
+        // 6) Push runnable consumers in the list of nodes to run and update the
+        // consumer producer system.
+        // At this point, simultaneously runnable consumers have no data
+        // dependency and could be run in parallel!
         for (const auto& runnable : runnableConsumers) {
-            if (verbose) printf("Runnable: %s\n", (runnable->type() + "_" + std::to_string(reinterpret_cast<uintptr_t>(runnable.get()))).c_str());
+            Log::debug("Runnable: {}", namePtrTable.at(runnable));
             runnable->getOperator()->updateConsummerProducer();
-            mStaticSchedule.push_back(runnable);
+            schedule.push_back(std::make_shared<StaticSchedulingElement>(runnable));
         }
 
-        // update producers and consumers list
-        if (verbose) printf("Updating producer and consumer lists...\n");
-        const auto oldConsumers = consumers;
-
-        for (const auto& consumer : oldConsumers) {
-            if (verbose) {
-                printf("\t- consumer: %s\n\t\tR/C:\t",
-                       (consumer->type() + "_" + std::to_string(reinterpret_cast<uintptr_t>(consumer.get()))).c_str());
-                for (IOIndex_t inId = 0; inId < consumer->nbInputs() - 1; ++inId) {
-                    printf("%ld/%ld\n\t\t\t", consumer->getOperator()->getNbConsumedData(inId),
-                           consumer->getOperator()->getNbRequiredData(inId));
-                }
-                printf("%zu/%zu", consumer->getOperator()->getNbConsumedData(static_cast<IOIndex_t>(consumer->nbInputs()) - 1),
-                       consumer->getOperator()->getNbRequiredData(static_cast<IOIndex_t>(consumer->nbInputs()) - 1));
-                printf("\n\t\tP:\t");
-                for (IOIndex_t outId = 0; outId < consumer->nbOutputs() - 1; ++outId) {
-                    printf("%zu\n\t\t\t", consumer->getOperator()->getNbProducedData(outId));
-                }
-                printf("%zu", consumer->getOperator()->getNbProducedData(static_cast<IOIndex_t>(consumer->nbOutputs()) - 1));
-                printf("\n");
-            }
+        // 7) Update consumers list
+        Log::debug("Updating producer and consumer lists...");
+        for (const auto& consumer : runnableConsumers) {
+            summarizeConsumerState(consumer, namePtrTable.at(consumer));  // debug print
+            // 7.1) If the current consumer has still data to consume, it will
+            // be put back in the consumers list once the remaining consumers
+            // have been exhausted.
             bool isStillConsumer = false;
+            for (IOIndex_t inputIdx = 0; inputIdx < consumer->nbInputs(); ++inputIdx) {
+                AIDGE_LOG_CONTEXT("Consumer node {} input #{}", namePtrTable.at(consumer), inputIdx);
+
+                if (consumer->getOperator()->getNbConsumedData(inputIdx) <
+                            getNbAvailableData(consumer, inputIdx)) {
+                    Log::debug("  still consumer: C{} < P{} for input #{}",
+                        consumer->getOperator()->getNbConsumedData(inputIdx),
+                        getNbAvailableData(consumer, inputIdx), inputIdx);
 
-            IOIndex_t parentID = 0;  // FIXME: handle this correctly
-            // should we check input or dataInput ?
-            for (const auto& consumerParent : consumer->inputs()) {
-                if (consumerParent.first &&
-                    consumer->getOperator()->getNbConsumedData(parentID++) <
-                            consumerParent.first->getOperator()->getNbProducedData(consumerParent.second)) {
                     // there is still data to consume
                     isStillConsumer = true;
                     break;
                 }
             }
 
+            // 7.2) If the current consumer becomes a producer for other nodes,
+            // its childs become consumers.
+            bool isProducer = false;
             for (IOIndex_t outId = 0; outId < consumer->nbOutputs(); ++outId) {
+                for (const auto& child : consumer->getChildren(outId)) {
+                    if (child) {
+                        IOIndex_t inputIdx = 0;
+                        for (const auto& childParent : child->getParents()) {
+                            if (childParent == consumer) {
+                                AIDGE_LOG_CONTEXT("Consumer node {} input #{} / Producer node {} output #{}",
+                                    namePtrTable.at(child), inputIdx, namePtrTable.at(consumer), outId);
+
+                                if (child->getOperator()->getNbConsumedData(inputIdx) < consumer->getOperator()->getNbProducedData(outId)) {
+                                    isProducer = true;
+                                    break;
+                                }
+                            }
+                            ++inputIdx;
+                        }
+
+                        if (isProducer) {
+                            break;
+                        }
+                    }
+                }
+/*
                 if (consumer->getOperator()->getNbProducedData(outId) > 0) {
-                    if (verbose) printf("  also producer\n");
+                    Log::debug("  also producer");
                     // make sure consumer is also a producer
                     producers.insert(consumer);
 
-                    const auto& childs = consumer->getChildren();
-                    consumers.insert(childs.begin(), childs.end());
+                    const auto& newConsumers = getConsumers({consumer});
+                    consumers.insert(newConsumers.cbegin(), newConsumers.cend());
                     break;
                 }
+*/
+            }
+
+            consumers.erase(consumer);
+
+            if (isProducer) {
+                Log::debug("  also producer");
+                // make sure consumer is also a producer
+                producers.insert(consumer);
+
+                const auto& newConsumers = getConsumers({consumer});
+                consumers.insert(newConsumers.cbegin(), newConsumers.cend());
             }
 
-            if (!isStillConsumer) {
-                if (verbose) printf("  no more consumer\n");
-                // consumer is no longer a consumer, only a producer
-                consumers.erase(consumer);
+            if (isStillConsumer) {
+                // If there is still data to consume, the consumer will be
+                // run AFTER the other remaining consumers
+                // (= non-greedy consumers)
+                stillConsumers.insert(consumer);
             }
         }
 
-        if (verbose) printf("*************\n");
+        // 8) If there is no more consumers, swap with possible "still consumers"
+        // This ensures that the "non-greedy" consumer behavior
+        if (consumers.empty()) {
+            consumers.swap(stillConsumers);
+            stillConsumers.clear();
+        }
+
+        Log::debug("********************");
     } while (!consumers.empty());
 
+    mPriorCache.clear();
+
+    if (!consumers.empty()) {
+        Log::warn("Remaining consumers: possible dead-lock");
+    }
+
+    return schedule;
+}
+
+
+void Aidge::Scheduler::summarizeConsumerState(const std::shared_ptr<Aidge::Node>& consumer, const std::string& nodeName) const {
+    Log::debug("\t- consumer: {}", fmt::styled(nodeName, fg(fmt::color::orange)));
+    std::string crLog = "\t\tC/R:\t";
+    for (IOIndex_t inId = 0; inId < consumer->nbInputs() - 1; ++inId) {
+        crLog += fmt::format("{}/{}\n\t\t\t", consumer->getOperator()->getNbConsumedData(inId),
+                consumer->getOperator()->getNbRequiredData(inId));
+    }
+    crLog += fmt::format("{}/{}", consumer->getOperator()->getNbConsumedData(static_cast<IOIndex_t>(consumer->nbInputs()) - 1),
+            consumer->getOperator()->getNbRequiredData(static_cast<IOIndex_t>(consumer->nbInputs()) - 1));
+    Log::debug("{}", crLog);
+
+    std::string pLog = "\t\tP:\t";
+    for (IOIndex_t outId = 0; outId < consumer->nbOutputs() - 1; ++outId) {
+        pLog += fmt::format("{}\n\t\t\t", consumer->getOperator()->getNbProducedData(outId));
+    }
+    pLog += fmt::format("{}", consumer->getOperator()->getNbProducedData(static_cast<IOIndex_t>(consumer->nbOutputs()) - 1));
+    Log::debug("{}", pLog);
 }
 
-// TODO: handle multiple inputs/outputs
-void Aidge::SequentialScheduler::forward(bool forwardDims, bool verbose) {
-    // Forward dims (if allowed)
-    if (forwardDims) {mGraphView->forwardDims(); }
 
-    // Generate scheduling *only if empty*
-    // If scheduling was already generated (in one or several steps, i.e. one or
-    // several successive call to generateScheduling()), do not generate it twice
-    if (mStaticSchedule.empty()) {
-        this->generateScheduling();
+void Aidge::Scheduler::generateEarlyLateScheduling(std::vector<std::shared_ptr<StaticSchedulingElement>>& schedule) const {
+    std::size_t latest = 0;
+    // Calculate early (logical) start
+    for (std::size_t elt = 0; elt < schedule.size(); ++elt) {
+        const auto node = schedule[elt]->node;
+        const auto itNode = std::find_if(schedule.rend() - elt, schedule.rend(),
+            [node](const auto& v) { return (v->node == node); });
+
+        // Node can be run the earliest just after its childs were run the last time!
+        std::size_t early = 0;
+        if (itNode != schedule.rend()) {
+            for (const auto& child : node->getChildren()) {
+                // Find child node next scheduled position
+                const auto it = std::find_if(schedule.rend() - elt, itNode,
+                    [child](const auto& v) { return (v->node == child); });
+                AIDGE_INTERNAL_ASSERT(it != schedule.rend());
+
+                const std::size_t step = std::distance(schedule.begin(), it.base()) - 1;
+                early = std::max(early, schedule[step]->early + 1);
+                schedule[step]->earlierThan.push_back(schedule[elt]);
+            }
+        }
+
+        // Node can be run the earliest just after its latest parent was run
+        for (const auto& parent : node->getParents()) {
+            // Find parent node latest scheduled position
+            const auto it = std::find_if(schedule.rend() - elt, schedule.rend(),
+                [parent](const auto& v) { return (v->node == parent); });
+            if (it != schedule.rend()) {
+                const std::size_t step = std::distance(schedule.begin(), it.base()) - 1;
+                early = std::max(early, schedule[step]->early + 1);
+                schedule[step]->earlierThan.push_back(schedule[elt]);
+            }
+        }
+
+        latest = std::max(latest, early);
+        schedule[elt]->early = early;
+    }
+
+    // Calculate late (logical) start
+    for (std::size_t elt = schedule.size(); elt-- != 0; ) {
+        const auto node = schedule[elt]->node;
+        const auto itNode = std::find_if(schedule.begin() + elt + 1, schedule.end(),
+            [node](const auto& v) { return (v->node == node); });
+
+        // Node can be run the latest just before its parents are run the next time!
+        std::size_t late = latest;
+        if (itNode != schedule.end()) {
+            for (const auto& parent : node->getParents()) {
+                // Find child node next scheduled position
+                const auto it = std::find_if(schedule.begin() + elt + 1, itNode,
+                    [parent](const auto& v) { return (v->node == parent); });
+                AIDGE_INTERNAL_ASSERT(it != schedule.end());
+
+                const std::size_t step = std::distance(schedule.begin(), it);
+                late = std::min(late, schedule[step]->late - 1);
+                schedule[step]->laterThan.push_back(schedule[elt]);
+            }
+        }
+
+        // Node can be run the latest just before its earliest child is run
+        for (const auto& child : node->getChildren()) {
+            // Find child node earliest scheduled position
+            const auto it = std::find_if(schedule.begin() + elt + 1, schedule.end(),
+                [child](const auto& v) { return (v->node == child); });
+            if (it != schedule.end()) {
+                const std::size_t step = std::distance(schedule.begin(), it);
+                late = std::min(late, schedule[step]->late - 1);
+                schedule[step]->laterThan.push_back(schedule[elt]);
+            }
+        }
+
+        schedule[elt]->late = late;
+    }
+}
+
+void Aidge::Scheduler::resetScheduling() {
+    for (auto node : mGraphView->getNodes()) {
+        node->getOperator()->resetConsummerProducer();
     }
 
-    // Clear previous scheduling results
+    mStaticSchedule.clear();
+    mStaticScheduleStep = 0;
     mScheduling.clear();
+}
+
+/**
+ * This version is a simplified version without special handling of concatenation.
+*/
+Aidge::MemoryManager Aidge::Scheduler::generateMemory(bool incProducers, bool wrapAroundBuffer) const {
+    MemoryManager memManager;
+
+    for (std::size_t step = 0; step < mStaticSchedule.size(); ++step) {
+        for (const auto& node : getStaticScheduling(step)) {
+            if (!incProducers && node->type() == Producer_Op::Type) {
+                memManager.releaseDependencies(node);
+                continue;
+            }
+
+            const auto childs = node->getChildren();
+            AIDGE_ASSERT(node->getOperator()->operatorType() == OperatorType::Tensor,
+                "Operator must be of Tensor type for node {} (of type {}).",
+                node->name(), node->type());
+            const auto op = std::static_pointer_cast<OperatorTensor>(node->getOperator());
+
+            std::vector<const MemoryManager::MemoryPlane*> wrapAroundMemPlane;
+
+            // Allocate a memory plane for each node's output
+            for (IOIndex_t outputIdx = 0; outputIdx < node->nbOutputs(); ++outputIdx) {
+                const auto requiredSize = op->getRequiredMemory(outputIdx, {});
+                AIDGE_ASSERT(requiredSize.type == Elts_t::Data,
+                    "Cannot generate memory with token-based producer-consumer model for node {} (of type {}).",
+                    node->name(), node->type());
+
+                // By default, specifies a fully monolithic memory block
+                std::size_t size = requiredSize.data;
+                std::size_t stride = 0;
+                std::size_t length = 1;
+                std::size_t count = 1;
+
+                if (op->getOutput(outputIdx) && op->getOutput(outputIdx)->dims().size() > 3) {
+                    // If it is possible, assume a NCHW layout
+                    size = op->getOutput(outputIdx)->dims().end()[-3];
+                    stride = size;
+                    length = op->getOutput(outputIdx)->dims().end()[-1];
+                    count = op->getOutput(outputIdx)->dims().end()[-2];
+                }
 
-    int cpt = 0;
-    for (const auto& runnable : mStaticSchedule) {
-        if (verbose)
-            printf("run: %s\n",
-                    (runnable->type() + "_" + std::to_string(reinterpret_cast<uintptr_t>(runnable.get()))).c_str());
-        else
-            drawProgressBar(static_cast<float>(cpt) / static_cast<float>(mStaticSchedule.size()), 50,
-                            (std::string("running ") + runnable->type() + "_" +
-                                std::to_string(reinterpret_cast<uintptr_t>(runnable.get()))));
-        const auto tStart = std::chrono::high_resolution_clock::now();
-        runnable->forward();
-        const auto tEnd = std::chrono::high_resolution_clock::now();
-        mScheduling.push_back(SchedulingElement(runnable, tStart, tEnd));
-        cpt++;
-    }
-    if (!verbose) drawProgressBar(1.0, 50, "                                   ");
-    printf("\n");
+                // Check if wrap around buffer is possible for this node
+                // (re-using previous node outputs memory for this node outputs).
+                // => only if this node is the only child of its parent(s)
+                std::size_t wrapAroundSize = 0;
+                std::size_t wrapAroundExtra = 0;
+                wrapAroundMemPlane.push_back(nullptr);
+
+                // Select the best parent among all allocable nodes for
+                // reallocation, which is the one with most memory (in order
+                // to minimize the reallocation size).
+                IOIndex_t inputIdx = 0;
+                for (const auto& parent : node->dataInputs()) {
+                    if (parent.first && parent.first->getChildren(parent.second).size() == 1
+                        // there might be no existing plane if the parent was
+                        // not yet scheduled (because it may be a recurrent connection)
+                        && memManager.getNbPlanes(parent.first) >= parent.first->nbOutputs()
+                        // memSpace should not be already released
+                        && memManager.getPlanes(parent.first).end()[-parent.first->nbOutputs()+parent.second].memSpace->released == -1)
+                    {
+                        const auto requiredData = op->getNbRequiredData(inputIdx);
+                        const auto requiredProtected = op->getNbRequiredProtected(inputIdx);
+                        AIDGE_ASSERT(requiredData.type == Elts_t::Data && requiredProtected.type == Elts_t::Data,
+                            "Cannot generate memory with token-based producer-consumer model for node {} (of type {}).",
+                            node->name(), node->type());
+
+                        const bool isWrappable = (requiredProtected.data < requiredData.data);
+                        const MemoryManager::MemoryPlane& memPlane = memManager.getPlanes(parent.first).end()[-parent.first->nbOutputs()+parent.second];
+
+                        if (isWrappable || !memManager.isWrapAround(
+                                    memPlane.memSpace,
+                                    memPlane.getFinalOffset()
+                                        - memPlane.memSpace->offset,
+                                    requiredSize.data))
+                        {
+                            if (memPlane.getSize() > wrapAroundSize + requiredProtected.data
+                                && std::find(wrapAroundMemPlane.begin(), wrapAroundMemPlane.end(), &memPlane) == wrapAroundMemPlane.end())
+                            {
+                                wrapAroundSize = memPlane.getSize() - requiredProtected.data;
+                                if (requiredSize.data > wrapAroundSize) {
+                                    wrapAroundExtra = requiredSize.data - wrapAroundSize;
+                                }
+                                wrapAroundMemPlane[outputIdx] = &memPlane;
+                            }
+
+                            if (wrapAroundExtra == 0) {
+                                break;
+                            }
+                        }
+                    }
+                    ++inputIdx;
+                }
+
+                // MemoryPlane to (re)use
+                const MemoryManager::MemoryPlane& memPlane
+                    = (wrapAroundBuffer && wrapAroundSize > 0)
+                        ? (*wrapAroundMemPlane[outputIdx]) :
+                            memManager.allocate(requiredSize.data, childs, stride, length, count);
+
+                if (wrapAroundBuffer && wrapAroundSize > 0) {
+                    memManager.reallocate(memPlane,
+                        node, 0,
+                        requiredSize.data, true, wrapAroundExtra, childs, stride, length, count);
+                }
+                else {
+                    memManager.reallocate(memPlane.memSpace,
+                        node, memPlane.offset,
+                        requiredSize.data, false, 0, childs, stride, length, count);
+                }
+            }
+
+            memManager.releaseDependencies(node);
+            memManager.tick();
+        }
+    }
+
+    return memManager;
+}
+
+void Aidge::Scheduler::connectInputs(std::vector<std::shared_ptr<Aidge::Tensor>> data){
+    // This version of connect inputs only connects tensor inputs in input data producers.
+    auto inputNodes = mGraphView->getOrderedInputs();
+
+    // Assert that the number of input data producers corresponds to the number of data input
+    assert(data.size() == inputNodes.size()  && "Scheduler connectInput error - Inconsistent number of graph inputs and inputs passed to the graph");
+
+    for (std::size_t i = 0; i < data.size(); ++i){
+        // TODO : maybe shallow copy instead of deepcopy
+        inputNodes[i].first->getOperator()->setInput(inputNodes[i].second, data[i]);
+    }
 }
 
-void Aidge::SequentialScheduler::saveSchedulingDiagram(const std::string& fileName) const {
-    FILE* fp = std::fopen((fileName + ".mmd").c_str(), "w");
-    std::fprintf(fp, "gantt\ndateFormat x\naxisFormat %%Q ms\n\n");
+void Aidge::Scheduler::saveSchedulingDiagram(const std::string& fileName) const {
+    auto fp = std::unique_ptr<FILE, decltype(&std::fclose)>(std::fopen((fileName + ".mmd").c_str(), "w"), &std::fclose);
+
+    if (!fp) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error,
+            "Could not create scheduling diagram log file: {}", fileName + ".mmd");
+    }
+
+    fmt::print(fp.get(), "gantt\ndateFormat x\naxisFormat %Q µs\n\n");
 
     if (!mScheduling.empty()) {
+        const std::map<std::shared_ptr<Node>, std::string> namePtrTable
+            = mGraphView->getRankedNodesName("{0} ({1}#{3})");
         const auto globalStart = mScheduling[0].start;
 
         for (const auto& element : mScheduling) {
-            std::fprintf(fp, "%s :%ld, %ld\n",
-                         (element.node->type() + "_" + std::to_string(reinterpret_cast<uintptr_t>(element.node.get())))
-                                 .c_str(),
+            auto name = namePtrTable.at(element.node);
+            // Mermaid does not allow : character in task title
+            std::replace(name.begin(), name.end(), ':', '_');
+
+            fmt::print(fp.get(), "{} :{}, {}\n",
+                         name,
                          std::chrono::duration_cast<std::chrono::microseconds>(element.start - globalStart).count(),
                          std::chrono::duration_cast<std::chrono::microseconds>(element.end - globalStart).count());
         }
     }
 
-    std::fprintf(fp, "\n");
-    std::fclose(fp);
+    fmt::print(fp.get(), "\n");
+}
+
+void Aidge::Scheduler::saveStaticSchedulingDiagram(const std::string& fileName) const {
+    auto fp = std::unique_ptr<FILE, decltype(&std::fclose)>(std::fopen((fileName + ".mmd").c_str(), "w"), &std::fclose);
+
+    if (!fp) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error,
+            "Could not create scheduling diagram log file: {}", fileName + ".mmd");
+    }
+
+    fmt::print(fp.get(), "gantt\ndateFormat x\naxisFormat %Q\n\n");
+
+    if (!mStaticSchedule.empty()) {
+        const std::map<std::shared_ptr<Node>, std::string> namePtrTable
+            = mGraphView->getRankedNodesName("{0} ({1}#{3})");
+
+        for (const auto& schedule : mStaticSchedule) {
+            for (const auto& element : schedule) {
+                auto name = namePtrTable.at(element->node);
+                // Mermaid does not allow : character in task title
+                std::replace(name.begin(), name.end(), ':', '_');
+
+                fmt::print(fp.get(), "{} :{}, {}\n",
+                            name, element->early, element->late);
+            }
+        }
+    }
+
+    fmt::print(fp.get(), "\n");
+}
+
+std::vector<std::shared_ptr<Aidge::Node>> Aidge::Scheduler::getStaticScheduling(std::size_t step) const {
+    const auto& staticSchedule = mStaticSchedule.at(step);
+    std::vector<std::shared_ptr<Node>> schedule;
+    std::transform(staticSchedule.begin(), staticSchedule.end(), std::back_inserter(schedule), [](const auto& v) { return v->node; });
+    return schedule;
 }
 
-std::set<std::shared_ptr<Aidge::Node>> Aidge::SequentialScheduler::getConsumers(
+std::set<std::shared_ptr<Aidge::Node>> Aidge::Scheduler::getConsumers(
         const std::set<std::shared_ptr<Node>>& producers) const {
     std::set<std::shared_ptr<Node>> consumers;
 
     for (const auto& producer : producers) {
         const auto& childs = producer->getChildren();
-        consumers.insert(childs.begin(), childs.end());
+        for (const auto& child : childs) {
+            // Do not schedule childs outside current graph!
+            if (mGraphView->inView(child)) {
+                consumers.insert(child);
+            }
+        }
     }
 
     return consumers;
 }
+
+Aidge::Elts_t Aidge::Scheduler::getNbAvailableData(const std::shared_ptr<Node>& node, const IOIndex_t inputIdx) const {
+    const auto parent = node->inputs()[inputIdx];
+
+    if (parent.first) {
+        // Parent is connected, everything if fine!
+        return parent.first->getOperator()->getNbProducedData(parent.second);
+    }
+    else if (std::shared_ptr<Node> upperNode = mUpperNode.lock()) {
+        // We are inside an upper operator (for instance a MetaOperator)
+        // We need to connect the "local" producer-consumer model to the upper
+        // one, by mapping local node inputs to the upper node inputs.
+        IOIndex_t nodeInputIdx = 0;
+        for (const auto& input : mGraphView->getOrderedInputs()) {
+            if (input.first == node) {
+                // Current node is an input
+                const auto upperInput = upperNode->inputs()[nodeInputIdx];
+                if (upperInput.first) {
+                    return upperInput.first->getOperator()->getNbProducedData(upperInput.second);
+                }
+            }
+            ++nodeInputIdx;
+        }
+    }
+
+    // Otherwise, two cases:
+    if (node->getOperator()->getRawInput(inputIdx)) {
+        // Input is not connected but a valid tensor exists
+        // => This means data was fed manually to the input, without a Producer
+        // In this case, we assume a single-use data (unlike a Producer, which
+        // keep producing the data each time it is needed).
+        fmt::print("No producer node attached to input#{} for node {} ({})\n", inputIdx, node->name(), node->type());
+        return Elts_t::DataElts(std::static_pointer_cast<Tensor>(node->getOperator()->getRawInput(inputIdx))->size());
+    }
+    else {
+        // Input is not connected, this is an error
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "Missing input#{} for node {} ({})\n", inputIdx, node->name(), node->type());
+    }
+
+    return Elts_t::NoneElts();
+}
+
+Aidge::Scheduler::PriorProducersConsumers Aidge::Scheduler::getPriorProducersConsumers(
+    const std::shared_ptr<Node>& node) const
+{
+    const auto priorCache = mPriorCache.find(node);
+    if (priorCache != mPriorCache.end()) {
+        return priorCache->second;
+    }
+
+    PriorProducersConsumers prior;
+
+    IOIndex_t inputIdx = 0;
+    for (const auto& parent : node->inputs()) {
+        if (parent.first) {
+            AIDGE_LOG_CONTEXT("Producer node {} (of type {}) output #{}",
+                parent.first->name(), parent.first->type(), parent.second);
+
+            if ((node->getOperator()->getNbConsumedData(inputIdx) + node->getOperator()->getNbRequiredData(inputIdx)) >
+                        parent.first->getOperator()->getNbProducedData(parent.second))
+            {
+                // the node needs more data than the current parent has provided yet
+                if (!mGraphView->inView(parent.first)) {
+                    // Do not schedule prior outside the current graph!
+                    // return PriorProducersConsumers(); // not scheduled
+                    prior.priorConsumers.insert(node);
+                }
+
+                else if (parent.first->type() == Producer_Op::Type) {
+                    prior.requiredProducers.insert(parent.first);
+                    prior.priorConsumers.insert(node);
+                }
+                else if (parent.first->type() == Memorize_Op::Type) {
+                    // Break cycles
+                    return PriorProducersConsumers(); // not scheduled
+                }
+                else {
+                    const auto& parentPrior = getPriorProducersConsumers(parent.first);
+
+                    if (!parentPrior.isPrior) {
+                        return PriorProducersConsumers(); // not scheduled
+                    }
+                    else {
+                        prior.requiredProducers.insert(parentPrior.requiredProducers.cbegin(), parentPrior.requiredProducers.cend());
+                        prior.priorConsumers.insert(parentPrior.priorConsumers.cbegin(), parentPrior.priorConsumers.cend());
+                    }
+                }
+            }
+        }
+        ++inputIdx;
+    }
+
+    prior.isPrior = true;
+    if (prior.priorConsumers.empty()) {
+        prior.priorConsumers.insert(node);
+    }
+    mPriorCache.insert(std::make_pair(node, prior));
+    return prior;
+}
diff --git a/src/scheduler/SequentialScheduler.cpp b/src/scheduler/SequentialScheduler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..801f46ffb0293696dad8a84908bdda2bbd789bfc
--- /dev/null
+++ b/src/scheduler/SequentialScheduler.cpp
@@ -0,0 +1,116 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include "aidge/scheduler/SequentialScheduler.hpp"
+
+#include <chrono>
+#include <memory>
+#include <set>
+#include <string>
+
+#include <fmt/ranges.h>
+#include <fmt/color.h>
+
+#include "aidge/graph/GraphView.hpp"
+#include "aidge/graph/Node.hpp"
+#include "aidge/utils/Types.h"
+#include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/operator/Producer.hpp"
+#include "aidge/operator/Memorize.hpp"
+#include "aidge/operator/MetaOperator.hpp"
+#include "aidge/recipes/GraphViewHelper.hpp"
+
+void Aidge::SequentialScheduler::forward(bool forwardDims, std::vector<std::shared_ptr<Aidge::Tensor>> data) {
+    // Collect all data input of the graph (that are producers)
+    if (!data.empty()){
+        connectInputs(data);
+    }
+
+    // Forward dims (if allowed)
+    if (forwardDims) {mGraphView->forwardDims(); }
+
+    // Generate scheduling *only if empty*
+    // If scheduling was already generated (in one or several steps, i.e. one or
+    // several successive call to generateScheduling()), do not generate it twice
+    if (mStaticSchedule.empty()) {
+        this->generateScheduling();
+    }
+
+    // Sort static scheduling according to the policy
+    std::vector<std::shared_ptr<StaticSchedulingElement>> staticSchedule(mStaticSchedule.at(mStaticScheduleStep).begin(), mStaticSchedule.at(mStaticScheduleStep).end());
+
+    if (mSchedulingPolicy == SchedulingPolicy::AsSoonAsPossible) {
+        std::stable_sort(staticSchedule.begin(), staticSchedule.end(),
+            [](const auto& lhs, const auto& rhs) { return (lhs->early < rhs->early); });
+    }
+    else if (mSchedulingPolicy == SchedulingPolicy::AsLateAsPossible) {
+        std::stable_sort(staticSchedule.begin(), staticSchedule.end(),
+            [](const auto& lhs, const auto& rhs) { return (lhs->late < rhs->late); });
+    }
+
+    const auto namePtrTable = mGraphView->getRankedNodesName("{0} ({1}#{3})");
+
+    for (const auto& runnable : staticSchedule) {
+        Log::debug("run: {}", namePtrTable.at(runnable->node));
+
+        const auto tStart = std::chrono::high_resolution_clock::now();
+        runnable->node->forward();
+        const auto tEnd = std::chrono::high_resolution_clock::now();
+        mScheduling.push_back(SchedulingElement(runnable->node, tStart, tEnd));
+    }
+
+    ++mStaticScheduleStep;
+    if (mStaticScheduleStep == mStaticSchedule.size()) {
+        mStaticScheduleStep = 0;
+    }
+}
+
+void Aidge::SequentialScheduler::backward(std::vector<std::shared_ptr<Aidge::Tensor>> data, bool instanciateGrad) {
+    // create ad set Grad values
+    if (instanciateGrad) { compile_gradient(mGraphView); }
+
+    const auto& ordered_outputs = mGraphView->getOrderedOutputs();
+    AIDGE_ASSERT(ordered_outputs.size() == data.size(), "You must provide the \
+                   right number of data objects to run the backward function. \
+                   {} outputs detected for the current GraphView when {} were \
+                   provided.", ordered_outputs.size(), data.size());
+    for (std::size_t i = 0; i < ordered_outputs.size(); ++i) {
+        const std::shared_ptr<OperatorTensor> op_ = std::dynamic_pointer_cast<OperatorTensor>(ordered_outputs[i].first->getOperator());
+        const std::shared_ptr<Tensor> t_grad = op_->getOutput(ordered_outputs[i].second)->grad();
+        AIDGE_ASSERT(data[i]->dims() == t_grad->dims(), "Wrong gradient size.");
+        *t_grad = data[i]->clone();
+    }
+    // Generate scheduling *only if empty*
+    // If scheduling was already generated (in one or several steps, i.e. one or
+    // several successive call to generateScheduling()), do not generate it twice
+    if (mStaticSchedule.empty()) {
+        this->generateScheduling();
+    }
+
+    // map of node <-> info to display with verbose
+    const auto namePtrTable = mGraphView->getRankedNodesName("{0} ({1}#{3})");
+
+    // run scheduled operators in reverse order
+    const auto& runnableList = mStaticSchedule.at(mStaticScheduleStep);
+    for (auto runnable = runnableList.crbegin(); runnable != runnableList.crend(); ++runnable) {
+        Log::debug("run: {}", namePtrTable.at((*runnable)->node));
+
+        const auto tStart = std::chrono::high_resolution_clock::now();
+        (*runnable)->node->backward();
+        const auto tEnd = std::chrono::high_resolution_clock::now();
+        mScheduling.push_back(SchedulingElement((*runnable)->node, tStart, tEnd));
+    }
+
+    ++mStaticScheduleStep;
+    if (mStaticScheduleStep == mStaticSchedule.size()) {
+        mStaticScheduleStep = 0;
+    }
+}
diff --git a/src/scheduler/ThreadPool.cpp b/src/scheduler/ThreadPool.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e81ab7a76f8a063b3ef33f5b24ecd2396267852e
--- /dev/null
+++ b/src/scheduler/ThreadPool.cpp
@@ -0,0 +1,65 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include "aidge/scheduler/ThreadPool.hpp"
+
+Aidge::ThreadPool::ThreadPool(size_t nbThreads) {
+    for (size_t i = 0; i < nbThreads; ++i) {
+        mThreads.emplace_back(std::thread(&ThreadPool::threadLoop, this));
+    }
+}
+
+void Aidge::ThreadPool::threadLoop() {
+    while (true) {
+        std::function<void()> job;
+        {
+            std::unique_lock<std::mutex> lock(mQueueMutex);
+            mMutexCondition.wait(lock, [this] {
+                return !mJobs.empty() || mTerminate;
+            });
+            if (mTerminate) {
+                return;
+            }
+            job = mJobs.front();
+            mJobs.pop();
+        }
+        job();
+    }
+}
+
+void Aidge::ThreadPool::queueJob(const std::function<void()>& job) {
+    {
+        std::unique_lock<std::mutex> lock(mQueueMutex);
+        mJobs.push(job);
+    }
+    mMutexCondition.notify_one();
+}
+
+bool Aidge::ThreadPool::busy() {
+    bool poolbusy;
+    {
+        std::unique_lock<std::mutex> lock(mQueueMutex);
+        poolbusy = !mJobs.empty();
+    }
+    return poolbusy;
+}
+
+Aidge::ThreadPool::~ThreadPool() {
+    {
+        std::unique_lock<std::mutex> lock(mQueueMutex);
+        mTerminate = true;
+    }
+    mMutexCondition.notify_all();
+    for (std::thread& active_thread : mThreads) {
+        active_thread.join();
+    }
+    mThreads.clear();
+}
diff --git a/src/stimuli/Stimulus.cpp b/src/stimuli/Stimulus.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6a91534475f6aaff44d5a2cd4da013434a99f9bf
--- /dev/null
+++ b/src/stimuli/Stimulus.cpp
@@ -0,0 +1,30 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include "aidge/stimuli/Stimulus.hpp"
+
+#include <memory>
+
+#include "aidge/data/Tensor.hpp"
+
+Aidge::Stimulus::~Stimulus() = default;
+
+std::shared_ptr<Aidge::Tensor> Aidge::Stimulus::load() {
+    AIDGE_ASSERT((mImpl!=nullptr || mData!=nullptr), "No load implementation and No stored data");
+
+    if (mLoadDataInMemory){
+        if (mData == nullptr){
+            mData = mImpl->load();
+        }
+        return mData;
+    }
+    return mImpl->load();
+}
\ No newline at end of file
diff --git a/src/utils/Log.cpp b/src/utils/Log.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..03ecded8f5a193a8ab00cf9dc7be502b98205de2
--- /dev/null
+++ b/src/utils/Log.cpp
@@ -0,0 +1,96 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include "aidge/utils/Log.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+
+#include <cstdlib>
+
+#include <fmt/color.h>
+#include <fmt/chrono.h>
+
+Aidge::Log::Level Aidge::Log::mConsoleLevel = []() {
+    const char* logLevel = std::getenv("AIDGE_LOGLEVEL_CONSOLE");
+    if (logLevel != nullptr) {
+        for (std::size_t i = 0; i < size(EnumStrings<Log::Level>::data); ++i) {
+            if (std::string(logLevel) == EnumStrings<Log::Level>::data[i]) {
+                return static_cast<Log::Level>(i);
+            }
+        }
+    }
+    return Info;
+}();
+Aidge::Log::Level Aidge::Log::mFileLevel = []() {
+    const char* logLevel = std::getenv("AIDGE_LOGLEVEL_FILE");
+    if (logLevel != nullptr) {
+        for (std::size_t i = 0; i < size(EnumStrings<Log::Level>::data); ++i) {
+            if (std::string(logLevel) == EnumStrings<Log::Level>::data[i]) {
+                return static_cast<Log::Level>(i);
+            }
+        }
+    }
+    return Debug;
+}();
+std::string Aidge::Log::mFileName = []() {
+    const char* logFile = std::getenv("AIDGE_LOG_FILE");
+    if (logFile != nullptr) {
+        return std::string(logFile);
+    }
+    return std::string();
+}();
+std::unique_ptr<FILE, decltype(&std::fclose)> Aidge::Log::mFile {nullptr, nullptr};
+std::vector<std::string> Aidge::Log::mContext;
+
+void Aidge::Log::log(Level level, const std::string& msg) {
+    if (level >= mConsoleLevel) {
+        // Apply log level style only for console.
+        // Styles that were already applied to msg with fmt are kept also in 
+        // the log file.
+        const auto modifier
+            = (level == Debug) ? fmt::fg(fmt::color::gray)
+            : (level == Notice) ? fmt::fg(fmt::color::light_yellow)
+            : (level == Warn) ? fmt::fg(fmt::color::orange)
+            : (level == Error) ? fmt::fg(fmt::color::red)
+            : (level == Fatal) ? fmt::bg(fmt::color::red)
+            : fmt::text_style();
+
+        for (const auto& context : mContext) {
+            fmt::println("Context: {}", context);
+        }
+
+        fmt::println("{}", fmt::styled(msg, modifier));
+    }
+
+    if (level >= mFileLevel && !mFileName.empty()) {
+        if (!mFile) {
+            initFile(mFileName);
+        }
+
+        for (const auto& context : mContext) {
+            fmt::println("Context: {}", context);
+        }
+
+        fmt::println(mFile.get(), msg);
+    }
+}
+
+void Aidge::Log::initFile(const std::string& fileName) {
+    mFile = std::unique_ptr<FILE, decltype(&std::fclose)>(std::fopen(fileName.c_str(), "a"), &std::fclose);
+
+    if (!mFile) {
+        mFileName.clear(); // prevents AIDGE_THROW_OR_ABORT() to try to log into file
+        AIDGE_THROW_OR_ABORT(std::runtime_error,
+            "Could not create log file: {}", fileName);
+    }
+
+    const std::time_t t = std::time(nullptr);
+    fmt::println(mFile.get(), "###### {:%Y-%m-%d %H:%M:%S} ######", fmt::localtime(t));
+}
diff --git a/src/utils/Random.cpp b/src/utils/Random.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0c3dc61df54e16d129638c66b4c245d6141e819c
--- /dev/null
+++ b/src/utils/Random.cpp
@@ -0,0 +1,22 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include "aidge/utils/Random.hpp"
+
+#include <random>  // normal_distribution, uniform_real_distribution
+
+std::mt19937 Aidge::Random::Generator::generator{std::random_device{}()};
+unsigned int Aidge::Random::Generator::seed = 0;
+
+void Aidge::Random::Generator::setSeed(unsigned int new_seed) {
+    seed = new_seed;
+    generator.seed(seed);
+}
diff --git a/unit_tests/CMakeLists.txt b/unit_tests/CMakeLists.txt
index 806f62d47dcad02614a18d0d7f6e51042b164cc8..9280d5fbdfd0a6a35724e5afd5caf672fefd8bf8 100644
--- a/unit_tests/CMakeLists.txt
+++ b/unit_tests/CMakeLists.txt
@@ -38,7 +38,7 @@ target_compile_options(tests${module_name} PUBLIC
     -fvisibility=hidden>)
 target_compile_options(tests${module_name} PRIVATE
 $<$<OR:$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:AppleClang>,$<CXX_COMPILER_ID:GNU>>:
--Wall -Wextra -Wold-style-cast -Winline -pedantic -Werror=narrowing -Wshadow $<$<BOOL:${WERROR}>:-Werror> ${SANITIZE_FLAGS}>)
+-Wall -Wextra -Wold-style-cast -pedantic -Werror=narrowing -Wshadow $<$<BOOL:${WERROR}>:-Werror> ${SANITIZE_FLAGS}>)
 target_compile_options(tests${module_name} PRIVATE
 $<$<CXX_COMPILER_ID:GNU>:${STRICT_ALIASING_FLAGS}>)
 target_compile_options(${module_name} PRIVATE
diff --git a/unit_tests/backend/Test_TensorImpl.cpp b/unit_tests/backend/Test_TensorImpl.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..43e25092a0f502698bbff7b0142969154f2cb0b0
--- /dev/null
+++ b/unit_tests/backend/Test_TensorImpl.cpp
@@ -0,0 +1,61 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <array>
+#include <cstddef>
+#include <cstdint>  //std::uint16_t
+#include <random>
+#include <vector>
+
+#include <catch2/catch_test_macros.hpp>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/TensorUtils.hpp"
+#include "aidge/backend/cpu/data/TensorImpl.hpp"
+
+using namespace Aidge;
+
+TEST_CASE("[backend/cpu/data] Tensor", "[TensorImpl]") {
+    Tensor x = Array3D<int, 2, 2, 2>{{{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}};
+
+    SECTION("Access to array") {
+        x = Array3D<int, 2, 2, 2>{{{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}};
+        REQUIRE(static_cast<int *>(x.getImpl()->rawPtr())[0] == 1);
+        REQUIRE(static_cast<int *>(x.getImpl()->rawPtr())[7] == 8);
+    }
+}
+
+TEST_CASE("Tensor fill", "[TensorImpl][fill]") {
+  SECTION("Instantiate batches independantly") {
+    // initialization with 0s
+    std::shared_ptr<Tensor> concatenatedTensor= std::make_shared<Tensor>(Array2D<int, 3, 5>{});
+    //concatenatedTensor->print();
+
+    std::shared_ptr<Tensor> myTensor1 = std::make_shared<Tensor>(Array1D<int, 5>{{1,2,3,4,5}});
+    std::shared_ptr<Tensor> myTensor2 = std::make_shared<Tensor>(Array1D<int, 5>{{6,7,8,9,10}});
+    std::shared_ptr<Tensor> myTensor3 = std::make_shared<Tensor>(Array1D<int, 5>{{11,12,13,14,15}});
+
+    // use copy function from implementation
+    concatenatedTensor->getImpl()->copy(myTensor1->getImpl()->rawPtr(), 5, 0);
+    concatenatedTensor->getImpl()->copy(myTensor2->getImpl()->rawPtr(), 5, 5);
+    concatenatedTensor->getImpl()->copy(myTensor3->getImpl()->rawPtr(), 5, 10);
+    // concatenatedTensor->print();
+
+    std::shared_ptr<Tensor> expectedTensor= std::make_shared<Tensor>(Array2D<int, 3, 5>{
+      {{1,2,3,4,5},
+      {6,7,8,9,10},
+      {11,12,13,14,15}}
+    });
+    // expectedTensor->print();
+
+    REQUIRE(*concatenatedTensor == *expectedTensor);
+  }
+}
diff --git a/unit_tests/data/Test_Tensor.cpp b/unit_tests/data/Test_Tensor.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..655fd725e9d7d913d24c6552571ae3b91e3605b4
--- /dev/null
+++ b/unit_tests/data/Test_Tensor.cpp
@@ -0,0 +1,412 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <array>
+#include <cstddef>     // std::size_t
+#include <cstdint>     // std::uint8_t, std::uint16_t, std::int32_t
+#include <numeric>     // std::accumulate, std::inner_product
+#include <functional>  // std::multiplies
+#include <random>      // std::random_device, std::mt19937,
+                       // std::uniform_int_distribution, std::uniform_real_distribution
+#include <set>
+#include <string>
+#include <vector>
+
+#include <catch2/catch_test_macros.hpp>
+
+#include "aidge/backend/cpu/data/TensorImpl.hpp"
+#include "aidge/data/Data.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/utils/ArrayHelpers.hpp"
+#include "aidge/utils/TensorUtils.hpp"
+#include "aidge/utils/Types.h"
+
+namespace Aidge {
+
+TEST_CASE("[core/data] Tensor(Construction)", "[Tensor][Constructor]") {
+    SECTION("Default constructor") {
+        Tensor T_default{};
+        REQUIRE((
+            (T_default.dataType() == DataType::Float32) &&
+            (T_default.size() == 1) &&
+            (T_default.dims() == std::vector<DimSize_t>({})) &&
+            (T_default.strides() == std::vector<DimSize_t>({1})) &&
+            (T_default.getImpl() == nullptr) &&
+            (T_default.grad() == nullptr) &&
+            (T_default.isContiguous() == true)
+        ));
+    }
+    SECTION("scalar constructor") {
+        Tensor T;
+        REQUIRE_NOTHROW(T = Tensor(std::int32_t(20)));
+        REQUIRE((
+            (T.dataType() == DataType::Int32) &&
+            (T.size() == 1) &&
+            (T.dims() == std::vector<DimSize_t>({})) &&
+            (T.strides() == std::vector<DimSize_t>({1})) &&
+            (T.getImpl() != nullptr) &&
+            (T.grad() == nullptr) &&
+            (T.isContiguous() == true)
+        ));
+    }
+    SECTION("dim constructor") {
+        const std::vector<DimSize_t> Tdims = {1,2,3,4,5,6,7};
+        Tensor T;
+        REQUIRE_NOTHROW(T = Tensor(Tdims));
+        REQUIRE((
+            (T.dataType() == DataType::Float32) &&
+            (T.size() == std::accumulate(Tdims.cbegin(), Tdims.cend(), DimSize_t(1), std::multiplies<DimSize_t>())) &&
+            (T.dims() == Tdims) &&
+            (T.strides() == std::vector<DimSize_t>({5040,2520,840,210,42,7,1})) &&
+            (T.getImpl() == nullptr) &&
+            (T.grad() == nullptr) &&
+            (T.isContiguous() == true)
+        ));
+    }
+    SECTION("TensorUtils, constructor from const arrays") {
+        Tensor T;
+        // Construction from different types and sizes
+
+        // Set an already constructed Tensor
+        REQUIRE_NOTHROW(T = Array1D<int, 2>{{1, 2}});
+        REQUIRE((
+            (T.dataType() == DataType::Int32) &&
+            (T.size() == 2) &&
+            (T.dims() == std::vector<DimSize_t>({2})) &&
+            (T.strides() == std::vector<DimSize_t>({1})) &&
+            (T.getImpl() != nullptr) &&
+            (T.grad() == nullptr) &&
+            (T.isContiguous() == true)
+        ));
+
+        // Change dims
+        REQUIRE_NOTHROW(T = Array2D<int, 2, 2>{{{1, 2}, {3, 4}}});
+        // Change data types
+        REQUIRE_NOTHROW(T = Array3D<std::uint8_t, 2, 2, 2>{{{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}});
+        REQUIRE((
+            (T.dataType() == DataType::UInt8) &&
+            (T.size() == 8) &&
+            (T.dims() == std::vector<DimSize_t>({2,2,2})) &&
+            (T.strides() == std::vector<DimSize_t>({4,2,1})) &&
+            (T.getImpl() != nullptr) &&
+            (T.grad() == nullptr) &&
+            (T.isContiguous() == true)
+        ));
+        REQUIRE_NOTHROW(T = Array3D<int, 2, 2, 2>{{{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}});
+        REQUIRE_NOTHROW(T = Array3D<float, 2, 2, 2>{{{{1.0f, 2.0f}, {3.0f, 4.0f}}, {{5.0f, 6.0f}, {7.0f, 8.0f}}}});
+        REQUIRE_NOTHROW(T = Array3D<double, 2, 2, 2>{{{{1., 2.}, {3., 4.}}, {{5., 6.}, {7., 8.}}}});
+
+        // Change dims
+        REQUIRE_NOTHROW(T = Array4D<int, 2, 2, 2, 2>{{{{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}},
+                                                    {{{9,10}, {11,12}}, {{13,14},{15,16}}}}});
+        REQUIRE((
+            (T.dataType() == DataType::Int32) &&
+            (T.size() == 16) &&
+            (T.dims() == std::vector<DimSize_t>({2,2,2,2})) &&
+            (T.strides() == std::vector<DimSize_t>({8,4,2,1})) &&
+            (T.getImpl() != nullptr) &&
+            (T.grad() == nullptr) &&
+            (T.isContiguous() == true)
+        ));
+    }
+    SECTION("copy constructor / copy assignment operator") {
+
+    }
+    SECTION("move constructor / move assignment operator") {
+
+    }
+    SECTION("prototype") {
+        constexpr std::uint16_t NBTRIALS = 10;
+
+        // Create random number generators
+        std::random_device rd;
+        std::mt19937 gen(rd());
+        std::uniform_int_distribution<std::size_t> dimsDist(1, 10);
+        std::uniform_int_distribution<std::size_t> nbDimsDist(1, 5);
+        std::uniform_real_distribution<float> valueDist(0.001f, 1.0f);
+
+        for (std::size_t trial = 0; trial < NBTRIALS; ++trial) {
+            std::vector<std::size_t> Tdims;
+            const std::size_t Tsize = nbDimsDist(gen);
+            for (std::size_t i = 0; i < Tsize; ++i) {
+                Tdims.push_back(dimsDist(gen));
+            }
+            Tensor T(Tdims);
+
+            // file the tensor
+            std::unique_ptr<float[]> array0(new float[T.size()]);
+            for (std::size_t i = 0; i < T.size(); ++i) {
+                array0[i] = valueDist(gen);
+            }
+            T.setBackend("cpu");
+            T.getImpl() -> setRawPtr(array0.get(), T.size());
+
+            Tensor Tclone;
+            REQUIRE_NOTHROW(Tclone = T.clone());
+            REQUIRE((
+                (T.dataType() == Tclone.dataType()) &&
+                (T.size() == Tclone.size()) &&
+                (T.dims() == Tclone.dims()) &&
+                (T.strides() == Tclone.strides()) &&
+                (T.getImpl() != Tclone.getImpl()) &&
+                (Tclone.grad() == nullptr) &&
+                (Tclone.isContiguous() == true)
+            ));
+            REQUIRE(Tclone == T);
+        }
+    }
+}
+
+TEST_CASE("[core/data] Tensor(getter/setter)", "[Tensor][Getter][Setter]") {
+    constexpr std::uint16_t NBTRIALS = 10;
+
+    // Create random number generators
+    std::random_device rd;
+    std::mt19937 gen(rd());
+    std::uniform_int_distribution<std::size_t> dimsDist(1, 10);
+    std::uniform_int_distribution<std::size_t> nbDimsDist(1, 5);
+    std::uniform_real_distribution<float> valueDist(0.001f, 1.0f);
+
+    for (std::size_t trial = 0; trial < NBTRIALS; ++trial) {
+        std::vector<std::size_t> Tdims;
+        const std::size_t Tsize = nbDimsDist(gen);
+        for (std::size_t i = 0; i < Tsize; ++i) {
+            Tdims.push_back(dimsDist(gen));
+        }
+
+        // create Tensor
+        Tensor T(Tdims);
+        // compute stride
+        std::vector<std::size_t> Tstrides(Tdims.size(), 1);
+        std::size_t i = Tdims.size() - 1;
+        while (i-- > 0) {
+            Tstrides[i] = Tstrides[i+1]*Tdims[i+1];
+        }
+
+    /////////////////
+    // dimensions
+        // nbDims(), dims(), size()
+        REQUIRE(T.nbDims() == Tdims.size());
+
+        REQUIRE(T.dims() == Tdims);
+
+        std::size_t trueSize = std::accumulate(Tdims.cbegin(), Tdims.cend(), 1, std::multiplies<std::size_t>());
+        REQUIRE(T.size() == trueSize);
+
+    /////////////////
+    // implementation
+        // getImpl(), setImpl(), hasImpl()
+        REQUIRE(T.hasImpl() == false);
+        std::shared_ptr<TensorImpl_cpu<float>> tensorImpl = std::make_shared<TensorImpl_cpu<float>>(0, Tdims);
+
+        T.setImpl(tensorImpl);
+        REQUIRE(T.getImpl() == tensorImpl);
+        REQUIRE(T.hasImpl() == true);
+
+        // isContiguous(), stride(),
+        REQUIRE(T.isContiguous());
+        REQUIRE(T.strides() == Tstrides);
+
+        // file the tensor
+        std::unique_ptr<float[]> array0(new float[T.size()]);
+        for (std::size_t i = 0; i < T.size(); ++i) {
+            array0[i] = valueDist(gen);
+        }
+        tensorImpl -> setRawPtr(array0.get(), T.size());
+
+        // getCoord(), getIdx(), getStorageIdx()
+        std::vector<DimSize_t> Tdims_copy = Tdims;
+        for (auto& val : Tdims_copy) {
+            val = std::min(DimSize_t(2), std::max(DimSize_t(0), val - 1));
+        }
+        DimSize_t true_flatid = std::inner_product(Tdims_copy.cbegin(), Tdims_copy.cend(), Tstrides.cbegin(), DimSize_t(0));
+
+        REQUIRE(T.getCoord(true_flatid) == Tdims_copy);
+        REQUIRE(T.getIdx(Tdims_copy) == true_flatid);
+        REQUIRE(T.getStorageIdx(Tdims_copy) == true_flatid); // Tensor is not a view
+
+        // set(vector), set(size_t), get(vector), get(size_t), getImplOffset()
+        REQUIRE_NOTHROW(T.set<float>(Tdims_copy, 50.0f));
+        REQUIRE(T.get<float>(Tdims_copy) == 50.0f);
+
+        REQUIRE_NOTHROW(T.set<float>(true_flatid, 40.0f));
+        REQUIRE(T.get<float>(true_flatid) == 40.0f);
+        REQUIRE(T.getImplOffset() == 0);
+
+
+    //////////////
+    // backend
+        // getAvailableBackends()
+        REQUIRE(Tensor::getAvailableBackends() == std::set<std::string>({"cpu"}));
+
+        // setBackend()
+        REQUIRE_NOTHROW(T.setBackend("cpu", 0));
+
+        // setDataType(), dataType()
+        REQUIRE_NOTHROW(T.setDataType(DataType::Int16));
+        REQUIRE(T.dataType() == DataType::Int16);
+    }
+}
+TEST_CASE("[core/data] Tensor(other)", "[Tensor][extract][zeros][print]") {
+	// extract, makeContiguous
+	// empty
+    constexpr std::uint16_t NBTRIALS = 10;
+
+    // Create random number generators
+    std::random_device rd;
+    std::mt19937 gen(rd());
+    std::uniform_int_distribution<std::size_t> dimsDist(1, 10);
+    std::uniform_int_distribution<std::size_t> nbDimsDist(1, 5);
+    std::uniform_real_distribution<float> valueDist(0.001f, 1.0f);
+    // zeros, resize
+    SECTION("zeros") {
+        Tensor T;
+        for (std::size_t trial = 0; trial < NBTRIALS; ++trial) {
+            std::vector<std::size_t> Tdims;
+            const std::size_t Tsize = nbDimsDist(gen);
+            for (std::size_t i = 0; i < Tsize; ++i) {
+                Tdims.push_back(dimsDist(gen));
+            }
+            T.resize(Tdims);
+
+            // file the tensor
+            std::unique_ptr<float[]> array0(new float[T.size()]);
+            for (std::size_t i = 0; i < T.size(); ++i) {
+                array0[i] = valueDist(gen);
+            }
+            T.setBackend("cpu");
+            T.getImpl() -> setRawPtr(array0.get(), T.size());
+            float* res = static_cast<float*>(T.getImpl()->hostPtr());
+            for (std::size_t i = 0; i < T.size(); ++i) {
+                REQUIRE(res[i] == array0[i]);
+            }
+
+            T.zeros();
+            res = static_cast<float*>(T.getImpl()->hostPtr());
+            for (std::size_t i = 0; i < T.size(); ++i) {
+                REQUIRE(res[i] == 0.0f);
+            }
+        }
+    }
+
+    SECTION("Tensor extract") {
+        bool equal;
+
+        for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+            // create Tensor
+            const std::size_t nb_dims = 3;
+            const std::size_t dim0 = dimsDist(gen) + 1; // dim0 >= 2
+            const std::size_t dim1 = dimsDist(gen) + 1;
+            const std::size_t dim2 = dimsDist(gen) + 1;
+            std::vector<std::size_t> dims = {dim0, dim1, dim2};
+            std::unique_ptr<int[]> array0(new int[dim0*dim1*dim2]);
+            for (std::size_t i = 0; i < dim0; ++i) {
+                for (std::size_t j = 0; j < dim1; ++j) {
+                    for (std::size_t k = 0; k < dim2; ++k) {
+                        array0[((i * dim1) + j)*dim2 + k] = valueDist(gen);
+                    }
+                }
+            }
+            Tensor x{dims};
+            x.setDataType(DataType::Int32);
+            x.setBackend("cpu");
+            Tensor y;
+            Tensor y0;
+            Tensor y1;
+            Tensor y2;
+            Tensor y3;
+            x.getImpl()->setRawPtr(array0.get(), dim0*dim1*dim2);
+            REQUIRE(x.isContiguous());
+
+        ////////////////
+        // extract contiguous Tensor slice given start coordinates
+            // the whole Tensor
+            REQUIRE_NOTHROW(y0 = x.extract({}));
+            REQUIRE(y0 == x);
+            int* y0_res = static_cast<int*>(y0.getImpl()->hostPtr());
+            equal = true;
+            for (std::size_t i = 0; i < dim0*dim1*dim2; ++i) {
+                equal &= (y0_res[i] == array0[i]);
+            }
+            REQUIRE(equal);
+            REQUIRE(y0.getImpl() == x.getImpl());
+            REQUIRE(y0.isContiguous());
+
+            // Tensor - 1-D
+            REQUIRE_NOTHROW(y1 = x.extract({dim0 - 2}));
+            int* y1_res = static_cast<int*>(y1.getImpl()->hostPtr());
+            equal = true;
+            for (std::size_t i = 0; i < dim1*dim2; ++i) {
+                equal &= (y1_res[i] == array0[(dim0-2)*dim1*dim2 + i]);
+            }
+            REQUIRE(equal);
+            REQUIRE(y1.getImpl() == x.getImpl());
+            REQUIRE(y1.isContiguous());
+
+            // Tensor - 2-D
+            REQUIRE_NOTHROW(y2 = x.extract({dim0 - 2, dim1 - 2}));
+            int* y2_res = static_cast<int*>(y2.getImpl()->hostPtr());
+            equal = true;
+            for (std::size_t i = 0; i < dim2; ++i) {
+                equal &= (y2_res[i] == array0[(((dim0 - 2) * dim1) + (dim1 - 2))*dim2 + i]);
+            }
+            REQUIRE(equal);
+            REQUIRE(y2.getImpl() == x.getImpl());
+            REQUIRE(y2.isContiguous());
+
+            // Tensor - 3-D => scalar
+            REQUIRE_NOTHROW(y3 = x.extract({dim0 - 2, dim1 - 2, dim2 - 2}));
+            int* y3_res = static_cast<int*>(y3.getImpl()->hostPtr());
+            REQUIRE(y3_res[0] == array0[(((dim0 - 2) * dim1) + (dim1 - 2))*dim2 + dim2 - 2]);
+            REQUIRE(y3.getImpl() == x.getImpl());
+            REQUIRE(y3.isContiguous());
+
+            // throw an error
+            REQUIRE_THROWS(y = x.extract({0, dim1, 0}));
+
+        /////////////////
+        // extract Tensor slice given start coordinates and dimension
+            REQUIRE_NOTHROW(y = x.extract({0, 0, 1}, {dim0-1, 1, dim2-1}));
+            REQUIRE(y.getImpl() == x.getImpl()); // shared implem
+            REQUIRE(!y.isContiguous());
+
+            Tensor yClone = y.clone(); // when copying data, they are contiguous in memory
+            REQUIRE(yClone.isContiguous());
+            // int yTruth[2][1][1] =
+            REQUIRE(approxEq<int>(yClone, y, 0.0f, 0.0f));
+        }
+    }
+
+    // print, toString,
+    SECTION("Pretty printing for debug") {
+        Tensor x{};
+        // Empty Tensor
+        REQUIRE_THROWS(x.print());
+        // scalar
+        x = Tensor(42);
+        REQUIRE_NOTHROW(x.print());
+        // 1-D Tensors
+        x = Array1D<int, 1>{{1}};
+        REQUIRE_NOTHROW(x.print());
+        x = Array1D<int, 6>{{1,2,3,4,5,6}};
+        REQUIRE_NOTHROW(x.print());
+        // 2-D Tensors
+        x = Array2D<int, 3, 2>{{{1, 2}, {3, 4}, {5, 6}}};
+        REQUIRE_NOTHROW(x.print());
+        // +2-D Tensors
+        x = Array3D<int, 2, 2, 2>{{{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}};
+        REQUIRE_NOTHROW(x.print());
+        x = Array4D<int, 2, 2, 2, 2>{{{{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}},{{{11, 12}, {13, 14}}, {{15, 16}, {17, 18}}}}};
+        REQUIRE_NOTHROW(x.print());
+    }
+}
+
+} // namespace Aidge
diff --git a/unit_tests/graph/Test_GraphView.cpp b/unit_tests/graph/Test_GraphView.cpp
index ebbfb3ad89721eb4f1390c3efca475acbb0b6f46..437780b959b37e0cf6b5b7796e71c9b931f25bc0 100644
--- a/unit_tests/graph/Test_GraphView.cpp
+++ b/unit_tests/graph/Test_GraphView.cpp
@@ -74,7 +74,7 @@ TEST_CASE("genRandomGraph", "[GraphView][randomGen]") {
         }
     }
 
-    printf("nbUnicity = %zu/%zu\n", nbUnicity, nbTests);
+    fmt::print("nbUnicity = {}/{}\n", nbUnicity, nbTests);
 }
 
 TEST_CASE("clone", "[GraphView][clone]") {
@@ -147,7 +147,7 @@ TEST_CASE("clone_with_delete", "[GraphView][cloneDelete]") {
         ++seed;
     }
 
-    printf("nbClonedWithDelete = %zu/%zu\n", nbClonedWithDelete, nbTests);
+    fmt::print("nbClonedWithDelete = {}/{}\n", nbClonedWithDelete, nbTests);
 }
 
 TEST_CASE("remove", "[GraphView][remove]") {
@@ -205,7 +205,7 @@ TEST_CASE("remove", "[GraphView][remove]") {
         }
     }
 
-    printf("nbTested = %zu/%zu\n", nbTested, nbTests);
+    fmt::print("nbTested = {}/{}\n", nbTested, nbTests);
 }
 
 TEST_CASE("[core/graph] GraphView(Constructor)", "[GraphView][constructor()]") {
@@ -381,7 +381,7 @@ TEST_CASE("[core/graph] GraphView(save)") {
     g1->addChild(conv5, "c4", 0, 0);
 
     g1->save("./graphExample");
-    printf("File saved in ./graphExample.md\n");
+    fmt::print("File saved in ./graphExample.md\n");
 }
 
 TEST_CASE("[core/graph] GraphView(resetConnections)") {
diff --git a/unit_tests/graphRegex/Test_GraphRegex.cpp b/unit_tests/graphRegex/Test_GraphRegex.cpp
index 924aac79ea8492f6ea0f2cd4d93676876c5a8331..a62b9a8602b494f26fb47061b899eaba41129a1f 100644
--- a/unit_tests/graphRegex/Test_GraphRegex.cpp
+++ b/unit_tests/graphRegex/Test_GraphRegex.cpp
@@ -9,7 +9,7 @@
 #include "aidge/operator/FC.hpp"
 #include "aidge/operator/MatMul.hpp"
 #include "aidge/operator/Producer.hpp"
-#include "aidge/recipies/Recipies.hpp"
+#include "aidge/recipes/Recipes.hpp"
 
 #include "aidge/operator/Conv.hpp"
 #include "aidge/operator/GenericOperator.hpp"
@@ -18,6 +18,32 @@ using namespace Aidge;
 
 TEST_CASE("GraphRegexUser") {
 
+
+    SECTION("Match using custom lambda") {
+
+        std::shared_ptr<GraphView> g1 = std::make_shared<GraphView>("TestGraph");
+        std::shared_ptr<Node> conv = GenericOperator("Conv", 1, 0, 1, "c");
+        std::shared_ptr<Node> fc = GenericOperator("FC", 1, 0, 1, "c1");
+        std::shared_ptr<Node> conv2 = GenericOperator("Conv", 1, 0, 1, "c2");
+        std::shared_ptr<Node> fc2 = GenericOperator("FC", 1, 0, 1, "c3");
+
+        g1->add(conv);
+        g1->addChild(fc, "c");
+        g1->addChild(conv2, "c1");
+        g1->addChild(fc2, "c2");
+        
+        ///
+        std::shared_ptr<GraphRegex> sut = std::make_shared<GraphRegex>();
+        sut->setNodeKey("C",+[](NodePtr NodeOp){return NodeOp->type() == "FC";});
+        
+        sut->setNodeKey("A","C($)==True");
+        sut->addQuery("A");
+        auto match = sut->match(g1);
+        REQUIRE(match.size() == 2);
+
+    }
+
+
     SECTION("INIT") {
 
         const std::string query = "Conv->FC";
@@ -126,9 +152,9 @@ TEST_CASE("GraphRegexUser") {
     SECTION("Applied Recipes"){
 
       // generate the original GraphView
-        auto matmul0 = MatMul(5, 5, "matmul0");
+        auto matmul0 = MatMul("matmul0");
         auto add0 = Add(2, "add0");
-        auto matmul1 = MatMul(5, 5, "matmul1");
+        auto matmul1 = MatMul("matmul1");
         auto add1 = Add(2, "add1");
 
         auto b0 = Producer({5}, "B0");
@@ -154,7 +180,7 @@ TEST_CASE("GraphRegexUser") {
 
 
         auto g = std::make_shared<GraphView>();
-        g->add({matmul0, add0, matmul1, add1, b0, b1,fl,fc});
+        g->add({w0, matmul0, b0, add0, w1, matmul1, b1, add1,fl,fc});
 
         std::shared_ptr<GraphRegex> kitchenBook = std::make_shared<GraphRegex>();
 
diff --git a/unit_tests/graphRegex/Test_examples.cpp b/unit_tests/graphRegex/Test_examples.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d85ae5c893a7ae4497125a62dad3cde97dac5195
--- /dev/null
+++ b/unit_tests/graphRegex/Test_examples.cpp
@@ -0,0 +1,55 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <catch2/catch_test_macros.hpp>
+#include <set>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/graph/GraphView.hpp"
+#include "aidge/graph/OpArgs.hpp"
+#include "aidge/operator/ReLU.hpp"
+#include "aidge/operator/MetaOperatorDefs.hpp"
+#include "aidge/operator/Producer.hpp"
+#include "aidge/graphRegex/GraphRegex.hpp"
+#include "aidge/recipes/Recipes.hpp"
+
+namespace Aidge {
+
+
+TEST_CASE("Examples", "[GraphMatching]") {
+    auto g1 = Sequential({
+        Producer({16, 3, 512, 512}, "dataProvider"),
+        Conv(3, 4, {5, 5}, "conv1"),
+        ReLU(),
+        PaddedConv(4, 8, {5, 5}, "conv2", {1, 1}, {2, 2, 2, 2}),
+        ReLU(),
+        PaddedConv(8, 16, {5, 5}, "conv3", {1, 1}, {2, 2, 2, 2}),
+        ReLU()
+    });
+
+    expandMetaOps(g1);
+    g1->save("Test_examples");
+
+    auto regex = std::make_shared<GraphRegex>();
+    regex->setKeyFromGraph(g1);
+    regex->addQuery("Pad->Conv->ReLU");
+    // Won't work, wrong number of matches:
+    //regex->addQuery("Pad*->Conv->ReLU*");
+
+    const auto match = regex->match(g1);
+    REQUIRE(match.size() == 2);
+
+    for (const auto& solution : match) {
+        REQUIRE(solution->getAll().size() == 3);
+    }
+}
+
+}  // namespace Aidge
\ No newline at end of file
diff --git a/unit_tests/operator/Test_Div_Op.cpp b/unit_tests/operator/Test_Div_Op.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e659742c0bd200fa33b598f581cfef7b2f1e432e
--- /dev/null
+++ b/unit_tests/operator/Test_Div_Op.cpp
@@ -0,0 +1,144 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <catch2/catch_test_macros.hpp>
+#include <cstddef>  // std::size_t
+#include <memory>
+#include <random>   // std::random_device, std::mt19937, std::uniform_int_distribution
+#include <vector>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/operator/Div.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+
+namespace Aidge {
+TEST_CASE("[core/operator] Div_Op(computeOutputDims)", "[Div][computeOutputDims]") {
+    constexpr std::uint16_t NBTRIALS = 10;
+
+    // Create a random number generator
+    std::random_device rd;
+    std::mt19937 gen(rd());
+    std::uniform_int_distribution<std::size_t> dimsDist(1, 10);
+    std::uniform_int_distribution<std::size_t> nbDimsDist(1, 5);
+
+    // Create Div Operator
+    std::shared_ptr<Node> myDiv = Div();
+    auto op = std::static_pointer_cast<OperatorTensor>(myDiv -> getOperator());
+
+    // input_0
+    std::shared_ptr<Tensor> T0 = std::make_shared<Tensor>();
+    op -> associateInput(0,T0);
+    // input_1
+    std::shared_ptr<Tensor> T1 = std::make_shared<Tensor>();
+    op -> associateInput(1,T1);
+
+    /**
+     * @todo Special case: scalar not handled yet by
+     * ``OperatorTensor::computeOutputDims()``
+     */
+    // SECTION("Scalar / Scalar") {
+    //     // input_0
+    //     T0->resize({});
+
+    //     // input_1
+    //     T1->resize({});
+
+    //     REQUIRE_NOTHROW(op->computeOutputDims());
+    //     REQUIRE((op->getOutput(0)->dims() == std::vector<std::size_t>()));
+    // }
+    // SECTION("Scalar / +1-D") {
+    //     // a scalar is compatible with any other Tensor
+    //     // input_0
+    //     T0->resize({});
+
+    //     for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+
+    //         // input_1
+    //         const std::size_t nb_dims = nbDimsDist(gen);
+    //         std::vector<std::size_t> dims(nb_dims);
+    //         for (std::size_t i = 0; i < nb_dims; ++i) {
+    //             dims[i] = dimsDist(gen);
+    //         }
+    //         T1->resize(dims);
+
+    //         REQUIRE_NOTHROW(op->computeOutputDims());
+    //         REQUIRE((op->getOutput(0)->dims()) == dims);
+    //     }
+    // }
+    // SECTION("+1-D / Scalar") {
+    //     // a scalar is compatible with any other Tensor
+    //     // input_1
+    //     T1->resize({});
+
+    //     for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+
+    //         // input_0
+    //         const std::size_t nb_dims = nbDimsDist(gen);
+    //         std::vector<std::size_t> dims(nb_dims);
+    //         for (std::size_t i = 0; i < nb_dims; ++i) {
+    //             dims[i] = dimsDist(gen);
+    //         }
+    //         T0->resize(dims);
+
+    //         REQUIRE_NOTHROW(op->computeOutputDims());
+    //         REQUIRE((op->getOutput(0)->dims()) == dims);
+    //     }
+    // }
+    SECTION("+1-D / +1-D") {
+        // same size
+        for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+            const std::size_t nb_dims = nbDimsDist(gen) + 1;
+            std::vector<std::size_t> dims0(nb_dims);
+            for (std::size_t i = 0; i < nb_dims; ++i) {
+                dims0[i] = dimsDist(gen) + 1;
+            }
+
+            T0->resize(dims0);
+            T1->resize(dims0);
+            REQUIRE_NOTHROW(op->computeOutputDims());
+            REQUIRE((op->getOutput(0)->dims()) == dims0);
+        }
+
+        // broadcast
+        for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+            const std::size_t nb_dims = nbDimsDist(gen) + 1;
+            std::vector<std::size_t> dims0(nb_dims);
+            for (std::size_t i = 0; i < nb_dims; ++i) {
+                dims0[i] = dimsDist(gen) + 2;
+            }
+            std::vector<std::size_t> dimsOut = dims0;
+            std::vector<std::size_t> dims1 = dims0;
+            for (std::size_t i = 0; i < nb_dims; ++i) {
+                if (dimsDist(gen) <= 5) {
+                    dims1[i] = 1;
+                }
+            }
+            dims1.erase(dims1.cbegin(), dims1.cbegin() + std::min(nbDimsDist(gen), nb_dims-1));
+
+            T0->resize(dims0);
+            T1->resize(dims1);
+
+            REQUIRE_NOTHROW(op->computeOutputDims());
+            REQUIRE((op->getOutput(0)->dims()) == dimsOut);
+
+            // input_0 - wrong
+            // T1->resize({dims[0] + 1});
+            std::vector<std::size_t> dims1_wrong = dims1;
+            for (std::size_t i = 0; i < dims1.size(); ++i) {
+                ++dims1_wrong[i];
+            }
+            T1->resize(dims1_wrong);
+            REQUIRE(dims0 != dims1_wrong);
+            REQUIRE_THROWS(op->computeOutputDims());
+        }
+    }
+}
+} // namespace Aidge
diff --git a/unit_tests/operator/Test_GlobalAveragePooling_Op.cpp b/unit_tests/operator/Test_GlobalAveragePooling_Op.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fcd8489144be121633f2b0a9601dee171e2bdb5e
--- /dev/null
+++ b/unit_tests/operator/Test_GlobalAveragePooling_Op.cpp
@@ -0,0 +1,85 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <catch2/catch_test_macros.hpp>
+#include <cstddef> // std::size_t
+#include <memory>
+#include <random> // std::random_device, std::mt19937, std::uniform_int_distribution
+#include <vector>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/operator/GlobalAveragePooling.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+#include "aidge/utils/Types.h"
+
+namespace Aidge {
+TEST_CASE("[core/operator] GlobalAveragePooling_Op(computeOutputDims)",
+          "[GlobalAveragePooling][computeOutputDims]") {
+  constexpr std::uint16_t NB_TRIALS = 10;
+  // Create a random number generator
+  std::random_device rd;
+  std::mt19937 gen(rd());
+  std::uniform_int_distribution<std::size_t> dimsDist(1, 10);
+  std::uniform_int_distribution<std::size_t> inf3DimsDistribution(1, 2);
+  std::uniform_int_distribution<std::size_t> sup3DimsDistribution(3, 10);
+
+  // Create the GlobalAveragePooling Operator
+  std::shared_ptr<Node> myGlobAvgPool = GlobalAveragePooling();
+  auto op =
+      std::static_pointer_cast<OperatorTensor>(myGlobAvgPool->getOperator());
+
+  // input_0
+  std::shared_ptr<Tensor> input_T = std::make_shared<Tensor>();
+  SECTION("Un-connected input leads to failure.") {
+    REQUIRE_THROWS(op->computeOutputDims());
+  }
+  op->associateInput(0, input_T);
+
+  SECTION("Connected Inputs") {
+    SECTION("empty tensor") {
+      for (uint16_t trial = 0; trial < NB_TRIALS; ++trial) {
+        const std::size_t nb_dims = 0;
+        std::vector<std::size_t> dims(nb_dims);
+        input_T->resize(dims);
+        REQUIRE_NOTHROW(op->computeOutputDims());
+      }
+    }
+    SECTION("Full tensor") {
+      SECTION("nbDim < 3") {
+        for (uint16_t trial = 0; trial < NB_TRIALS; ++trial) {
+          const std::size_t nb_dims = inf3DimsDistribution(gen);
+          std::vector<std::size_t> dims(nb_dims);
+          for (uint16_t i = 0; i < nb_dims; ++i) {
+            dims[i] = dimsDist(gen);
+          }
+          input_T->resize(dims);
+          REQUIRE_THROWS(op->computeOutputDims());
+        }
+      }
+      SECTION("nbDim > 3") {
+        for (uint16_t trial = 0; trial < NB_TRIALS; ++trial) {
+          const std::size_t nb_dims = sup3DimsDistribution(gen);
+          std::vector<std::size_t> dims(nb_dims);
+          for (uint16_t i = 0; i < nb_dims; ++i) {
+            dims[i] = dimsDist(gen) + 1;
+          }
+          std::vector<DimSize_t> dims_out{dims[0], dims[1]};
+          input_T->resize(dims);
+          op->setInput(0, input_T);
+          REQUIRE_NOTHROW(op->computeOutputDims());
+          REQUIRE(op->getOutput(0)->dims() == dims_out);
+          REQUIRE((op->getOutput(0)->dims().size()) == static_cast<size_t>(2));
+        }
+      }
+    }
+  }
+}
+} // namespace Aidge
diff --git a/unit_tests/operator/Test_MatMul_Op.cpp b/unit_tests/operator/Test_MatMul_Op.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6c810e675ad46cc5580bd24e57f7e7dbb84db38f
--- /dev/null
+++ b/unit_tests/operator/Test_MatMul_Op.cpp
@@ -0,0 +1,196 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <catch2/catch_test_macros.hpp>
+#include <cstddef>  // std::size_t
+#include <memory>
+#include <random>   // std::random_device, std::mt19937, std::uniform_int_distribution
+#include <vector>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/operator/MatMul.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+
+namespace Aidge {
+TEST_CASE("[core/operator] MatMul_Op(computeOutputDims)", "[MatMul][computeOutputDims]") {
+    // Create a random number generator
+    std::random_device rd;
+    std::mt19937 gen(rd());
+    std::uniform_int_distribution<std::size_t> dist(1, 10);
+
+    // Create MatMul Operator
+    std::shared_ptr<Node> myMatMul = MatMul();
+    auto op = std::static_pointer_cast<OperatorTensor>(myMatMul -> getOperator());
+
+    /** @todo Special case of scalar Tensor objects.
+     * Not handled yet.
+    */
+    // SECTION("0-D / 0-D") {
+    //     std::shared_ptr<Tensor> T0 = std::make_shared<Tensor>();
+    //     T0->resize({});
+    //     op -> associateInput(0,T0);
+
+    //     // input_1 - right
+    //     std::shared_ptr<Tensor> T1 = std::make_shared<Tensor>();
+    //     T1->resize({});
+    //     op -> associateInput(1,T1);
+
+    //     REQUIRE_NOTHROW(op->computeOutputDims());
+    //     REQUIRE((op->getOutput(0)->dims()).empty());
+
+    //     // input_1 - wrong
+    //     T1->resize({dist(gen)});
+
+    //     REQUIRE_THROWS(op->computeOutputDims());
+    // }
+
+    SECTION("1-D / N-D") {
+        // input_0
+        std::shared_ptr<Tensor> T0 = std::make_shared<Tensor>();
+        const std::size_t dim0 = dist(gen);
+        T0->resize({dim0});
+        op -> associateInput(0,T0);
+
+        std::shared_ptr<Tensor> T1 = std::make_shared<Tensor>();
+        op -> associateInput(1,T1);
+
+        SECTION("1-D / 1-D") {
+            // input_1 - right
+            T1->resize({dim0});
+
+            REQUIRE_NOTHROW(op -> computeOutputDims());
+            REQUIRE((op->getOutput(0)->dims()).empty());
+
+            // input_1 - wrong
+            T1->resize({dim0+1});
+
+            REQUIRE_THROWS(op -> computeOutputDims());
+        }
+        SECTION("1-D / 2-D") {
+            // input_1 - right
+            const std::size_t dim1 = dist(gen);
+            T1->resize({dim0,dim1});
+
+            REQUIRE_NOTHROW(op -> computeOutputDims());
+            REQUIRE(op->getOutput(0)->dims() == std::vector<std::size_t>({dim1}));
+
+            // input_1 - wrong
+            T1->resize({dim0+1,dim1});
+
+            REQUIRE_THROWS(op -> computeOutputDims());
+        }
+        SECTION("1-D / +2-D") {
+            // input_1 - right
+            const std::size_t dim1 = dist(gen);
+            const std::size_t dim2 = dist(gen);
+            const std::size_t dim3 = dist(gen);
+            T1->resize({dim1,dim2,dim0,dim3});
+
+            REQUIRE_NOTHROW(op -> computeOutputDims());
+            REQUIRE(op->getOutput(0)->dims() == std::vector<std::size_t>({dim1,dim2,dim3}));
+        }
+    }
+    SECTION("2-D / N-D") {
+        // input_0
+        std::shared_ptr<Tensor> T0 = std::make_shared<Tensor>();
+        const std::size_t dim0 = dist(gen);
+        const std::size_t dim1 = dist(gen);
+        T0->resize({dim0,dim1});
+        op -> associateInput(0,T0);
+
+        // input_1
+        std::shared_ptr<Tensor> T1 = std::make_shared<Tensor>();
+        op -> associateInput(1,T1);
+
+        SECTION("2-D / 1-D") {
+            // input_1 - right
+            T1->resize({dim1});
+
+            REQUIRE_NOTHROW(op -> computeOutputDims());
+            REQUIRE(op->getOutput(0)->dims() == std::vector<std::size_t>({dim0}));
+
+            // input_1 - wrong
+            T1->resize({dim1+1});
+
+            REQUIRE_THROWS(op -> computeOutputDims());
+        }
+        SECTION("2-D / 2-D") {
+            // input_1 - right
+            const std::size_t dim2 = dist(gen);
+            T1->resize({dim1, dim2});
+
+            REQUIRE_NOTHROW(op -> computeOutputDims());
+            REQUIRE(op->getOutput(0)->dims() == std::vector<std::size_t>({dim0,dim2}));
+
+            // input_1 - wrong
+            T1->resize({dim1+1,dim2});
+
+            REQUIRE_THROWS(op -> computeOutputDims());
+        }
+        SECTION("2-D / +2-D") {
+            // input_1 - right
+            const std::size_t dim2 = dist(gen);
+            const std::size_t dim3 = dist(gen);
+            const std::size_t dim4 = dist(gen);
+            T1->resize({dim3,dim4,dim1, dim2});
+
+            REQUIRE_NOTHROW(op -> computeOutputDims());
+            REQUIRE(op->getOutput(0)->dims() == std::vector<std::size_t>({dim3,dim4,dim0,dim2}));
+
+            // input_1 - wrong
+            T1->resize({dim3,dim4,dim1+1,dim2});
+
+            REQUIRE_THROWS(op -> computeOutputDims());
+        }
+    }
+    SECTION("+2-D / +2-D") {
+        // input_0
+        std::shared_ptr<Tensor> T0 = std::make_shared<Tensor>();
+        const std::size_t dim0 = dist(gen) + 1;
+        const std::size_t dim1 = 1;
+        const std::size_t dim2 = dist(gen);
+        const std::size_t dim3 = dist(gen);
+        T0->resize({dim0,dim1,dim2,dim3});
+        op -> associateInput(0,T0);
+
+        // input_1
+        std::shared_ptr<Tensor> T1 = std::make_shared<Tensor>();
+        op -> associateInput(1,T1);
+
+        // input_1 - right
+        // 1
+        const std::size_t dim5 = dist(gen);
+        T1->resize({dim0,dim1,dim3,dim5});
+        REQUIRE_NOTHROW(op -> computeOutputDims());
+        REQUIRE(op->getOutput(0)->dims() == std::vector<std::size_t>({dim0,dim1,dim2,dim5}));
+
+        // 2 - input_1 broadcast
+        T1->resize({1,dim1,dim3,dim5});
+        REQUIRE_NOTHROW(op -> computeOutputDims());
+        REQUIRE(op->getOutput(0)->dims() == std::vector<std::size_t>({dim0,dim1,dim2,dim5}));
+
+        // 3 - input_0 broadcast
+        const std::size_t dim1_bigger = dist(gen) + 1;
+        T1->resize({dim0,dim1_bigger,dim3,dim5});
+        REQUIRE_NOTHROW(op -> computeOutputDims());
+        REQUIRE(op->getOutput(0)->dims() == std::vector<std::size_t>({dim0,dim1_bigger,dim2,dim5}));
+
+        // 4 - input_0+input_1 broadcast
+        T1->resize({1,dim1_bigger,dim3,dim5});
+        REQUIRE_NOTHROW(op -> computeOutputDims());
+        REQUIRE(op->getOutput(0)->dims() == std::vector<std::size_t>({dim0,dim1_bigger,dim2,dim5}));
+
+        // input_1 - wrong
+        T1->resize({dim0+1,dim1,dim3,dim5});
+        REQUIRE_THROWS(op -> computeOutputDims());
+    }
+}
+} // namespace Aidge
\ No newline at end of file
diff --git a/unit_tests/operator/Test_MetaOperator.cpp b/unit_tests/operator/Test_MetaOperator.cpp
index 68e2d4d4d5b4fe1b40f83c087eb61c7865d3db75..cd42791e0db1d95469bdd414cab94f1c6e8fea17 100644
--- a/unit_tests/operator/Test_MetaOperator.cpp
+++ b/unit_tests/operator/Test_MetaOperator.cpp
@@ -11,15 +11,17 @@
 
 #include <catch2/catch_test_macros.hpp>
 
+#include "aidge/operator/Pop.hpp"
 #include "aidge/operator/MetaOperator.hpp"
 #include "aidge/operator/MetaOperatorDefs.hpp"
 #include "aidge/graph/GraphView.hpp"
 #include "aidge/graph/Testing.hpp"
+#include "aidge/recipes/Recipes.hpp"
 #include <cstddef>
 
 using namespace Aidge;
 
-TEST_CASE("[core/operators] MetaOperator", "[Operator]") {
+TEST_CASE("[core/operators] MetaOperator", "[Operator][MetaOperator]") {
     SECTION("PaddedConv") {
         auto op = PaddedConv(1, 3, {3, 3}, "padded_conv", {1, 1}, {1, 1, 1, 1});
 
@@ -51,4 +53,78 @@ TEST_CASE("[core/operators] MetaOperator", "[Operator]") {
         //auto microGraphScheduler = std::dynamic_pointer_cast<MetaOperator_Op>(op->getOperator())->getMicroGraphScheduler();
         //REQUIRE(microGraphScheduler->getStaticScheduling().size() == 2);
     }
+
+    SECTION("LSTM") {
+        auto myLSTM = LSTM(32, 64, 16, true, "ltsm");
+        auto op = std::static_pointer_cast<OperatorTensor>(myLSTM->getOperator());
+
+        auto microGraph = std::dynamic_pointer_cast<MetaOperator_Op>(op)->getMicroGraph();
+        microGraph->save("lstm", false, false);
+
+        REQUIRE(myLSTM->nbInputs() == 3 + 8 + 8);
+        REQUIRE(myLSTM->nbData() == 1);
+        REQUIRE(myLSTM->nbOutputs() == 2);
+
+        std::shared_ptr<Tensor> myInput = std::make_shared<Tensor>();
+        myInput->resize({32});
+        std::shared_ptr<Tensor> myInit = std::make_shared<Tensor>();
+        myInit->resize({1, 64});
+
+        op->associateInput(0, myInput);
+        op->associateInput(17, myInit);
+        op->associateInput(18, myInit);
+
+        op->computeOutputDims();
+        microGraph->save("lstm_dims", true, true);
+        REQUIRE(op->outputDimsForwarded());
+
+        //op->updateConsummerProducer();  // require implementation
+        //auto microGraphScheduler = std::dynamic_pointer_cast<MetaOperator_Op>(op)->getMicroGraphScheduler();
+        //microGraphScheduler->saveSchedulingDiagram("lstm_scheduling");
+    }
+
+    SECTION("LSTM(expanded)") {
+        auto pop = Pop();
+        auto myLSTM = LSTM(2, 3, 2, true, "ltsm");
+        auto myGraph = Sequential({pop, myLSTM});
+        auto op = std::static_pointer_cast<OperatorTensor>(myLSTM->getOperator());
+
+        REQUIRE(myLSTM->nbInputs() == 3 + 8 + 8);
+        REQUIRE(myLSTM->nbData() == 1);
+        REQUIRE(myLSTM->nbOutputs() == 2);
+
+        std::shared_ptr<Tensor> myInput = std::make_shared<Tensor>(
+            Array3D<float, 2, 3, 2>{{{{1.0, 2.0}, {3.0, 4.0}, {5.0, 6.0}}, {{2.0, 3.0}, {4.0, 5.0}, {6.0, 7.0}}}});
+        std::shared_ptr<Tensor> myInit = std::make_shared<Tensor>(
+            Array2D<float, 3, 3>{{{0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}}});
+        std::shared_ptr<Tensor> myInitW = std::make_shared<Tensor>(
+            Array2D<float, 3, 2>{{{0.1, 0.1}, {0.1, 0.1}, {0.1, 0.1}}});
+        std::shared_ptr<Tensor> myInitR = std::make_shared<Tensor>(
+            Array2D<float, 3, 3>{{{0.1, 0.1, 0.1}, {0.1, 0.1, 0.1}, {0.1, 0.1, 0.1}}});
+
+        pop->getOperator()->associateInput(0, myInput);
+        op->associateInput(17, myInit);
+        op->associateInput(18, myInit);
+
+        // Weights X
+        myLSTM->input(1).first->getOperator()->setOutput(0, myInitW);
+        op->setInput(2, myInitW);
+        op->setInput(3, myInitW);
+        op->setInput(4, myInitW);
+        // Weights H
+        op->setInput(5, myInitR);
+        op->setInput(6, myInitR);
+        op->setInput(7, myInitR);
+        op->setInput(8, myInitR);
+
+        auto g = getConnectedGraphView(myLSTM);
+        g->save("lstm_before_expand", true, true);
+
+        expandMetaOps(g);
+        g->setRootNode(pop);
+        REQUIRE(g->rootNode() == pop);
+        g->save("lstm_expanded", true, true);
+
+        REQUIRE(g->getNodes().size() == 41);
+    }
 }
diff --git a/unit_tests/operator/Test_Mul_Op.cpp b/unit_tests/operator/Test_Mul_Op.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d3e0c5e086fac9d31db817d628214e95d4e41a32
--- /dev/null
+++ b/unit_tests/operator/Test_Mul_Op.cpp
@@ -0,0 +1,144 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <catch2/catch_test_macros.hpp>
+#include <cstddef>  // std::size_t
+#include <memory>
+#include <random>   // std::random_device, std::mt19937, std::uniform_int_distribution
+#include <vector>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/operator/Mul.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+
+namespace Aidge {
+TEST_CASE("[core/operator] Mul_Op(computeOutputDims)", "[Mul][computeOutputDims]") {
+    constexpr std::uint16_t NBTRIALS = 10;
+
+    // Create a random number generator
+    std::random_device rd;
+    std::mt19937 gen(rd());
+    std::uniform_int_distribution<std::size_t> dimsDist(1, 10);
+    std::uniform_int_distribution<std::size_t> nbDimsDist(1, 5);
+
+    // Create Mul Operator
+    std::shared_ptr<Node> myMul = Mul();
+    auto op = std::static_pointer_cast<OperatorTensor>(myMul -> getOperator());
+
+    // input_0
+    std::shared_ptr<Tensor> T0 = std::make_shared<Tensor>();
+    op -> associateInput(0,T0);
+    // input_1
+    std::shared_ptr<Tensor> T1 = std::make_shared<Tensor>();
+    op -> associateInput(1,T1);
+
+    /**
+     * @todo Special case: scalar not handled yet by
+     * ``OperatorTensor::computeOutputDims()``
+     */
+    // SECTION("Scalar / Scalar") {
+    //     // input_0
+    //     T0->resize({});
+
+    //     // input_1
+    //     T1->resize({});
+
+    //     REQUIRE_NOTHROW(op->computeOutputDims());
+    //     REQUIRE((op->getOutput(0)->dims() == std::vector<std::size_t>()));
+    // }
+    // SECTION("Scalar / +1-D") {
+    //     // a scalar is compatible with any other Tensor
+    //     // input_0
+    //     T0->resize({});
+
+    //     for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+
+    //         // input_1
+    //         const std::size_t nb_dims = nbDimsDist(gen);
+    //         std::vector<std::size_t> dims(nb_dims);
+    //         for (std::size_t i = 0; i < nb_dims; ++i) {
+    //             dims[i] = dimsDist(gen);
+    //         }
+    //         T1->resize(dims);
+
+    //         REQUIRE_NOTHROW(op->computeOutputDims());
+    //         REQUIRE((op->getOutput(0)->dims()) == dims);
+    //     }
+    // }
+    // SECTION("+1-D / Scalar") {
+    //     // a scalar is compatible with any other Tensor
+    //     // input_1
+    //     T1->resize({});
+
+    //     for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+
+    //         // input_0
+    //         const std::size_t nb_dims = nbDimsDist(gen);
+    //         std::vector<std::size_t> dims(nb_dims);
+    //         for (std::size_t i = 0; i < nb_dims; ++i) {
+    //             dims[i] = dimsDist(gen);
+    //         }
+    //         T0->resize(dims);
+
+    //         REQUIRE_NOTHROW(op->computeOutputDims());
+    //         REQUIRE((op->getOutput(0)->dims()) == dims);
+    //     }
+    // }
+    SECTION("+1-D / +1-D") {
+        // same size
+        for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+            const std::size_t nb_dims = nbDimsDist(gen) + 1;
+            std::vector<std::size_t> dims0(nb_dims);
+            for (std::size_t i = 0; i < nb_dims; ++i) {
+                dims0[i] = dimsDist(gen) + 1;
+            }
+
+            T0->resize(dims0);
+            T1->resize(dims0);
+            REQUIRE_NOTHROW(op->computeOutputDims());
+            REQUIRE((op->getOutput(0)->dims()) == dims0);
+        }
+
+        // broadcast
+        for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+            const std::size_t nb_dims = nbDimsDist(gen) + 1;
+            std::vector<std::size_t> dims0(nb_dims);
+            for (std::size_t i = 0; i < nb_dims; ++i) {
+                dims0[i] = dimsDist(gen) + 2;
+            }
+            std::vector<std::size_t> dimsOut = dims0;
+            std::vector<std::size_t> dims1 = dims0;
+            for (std::size_t i = 0; i < nb_dims; ++i) {
+                if (dimsDist(gen) <= 5) {
+                    dims1[i] = 1;
+                }
+            }
+            dims1.erase(dims1.cbegin(), dims1.cbegin() + std::min(nbDimsDist(gen), nb_dims-1));
+
+            T0->resize(dims0);
+            T1->resize(dims1);
+
+            REQUIRE_NOTHROW(op->computeOutputDims());
+            REQUIRE((op->getOutput(0)->dims()) == dimsOut);
+
+            // input_0 - wrong
+            // T1->resize({dims[0] + 1});
+            std::vector<std::size_t> dims1_wrong = dims1;
+            for (std::size_t i = 0; i < dims1.size(); ++i) {
+                ++dims1_wrong[i];
+            }
+            T1->resize(dims1_wrong);
+            REQUIRE(dims0 != dims1_wrong);
+            REQUIRE_THROWS(op->computeOutputDims());
+        }
+    }
+}
+} // namespace Aidge
diff --git a/unit_tests/operator/Test_Pow_Op.cpp b/unit_tests/operator/Test_Pow_Op.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c77615c11e99c174707df21560044fdd3b6a3c42
--- /dev/null
+++ b/unit_tests/operator/Test_Pow_Op.cpp
@@ -0,0 +1,144 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <catch2/catch_test_macros.hpp>
+#include <cstddef>  // std::size_t
+#include <memory>
+#include <random>   // std::random_device, std::mt19937, std::uniform_int_distribution
+#include <vector>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/operator/Pow.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+
+namespace Aidge {
+TEST_CASE("[core/operator] Pow_Op(computeOutputDims)", "[Pow][computeOutputDims]") {
+    constexpr std::uint16_t NBTRIALS = 10;
+
+    // Create a random number generator
+    std::random_device rd;
+    std::mt19937 gen(rd());
+    std::uniform_int_distribution<std::size_t> dimsDist(1, 10);
+    std::uniform_int_distribution<std::size_t> nbDimsDist(1, 5);
+
+    // Create Pow Operator
+    std::shared_ptr<Node> myPow = Pow();
+    auto op = std::static_pointer_cast<OperatorTensor>(myPow -> getOperator());
+
+    // input_0
+    std::shared_ptr<Tensor> T0 = std::make_shared<Tensor>();
+    op -> associateInput(0,T0);
+    // input_1
+    std::shared_ptr<Tensor> T1 = std::make_shared<Tensor>();
+    op -> associateInput(1,T1);
+
+    /**
+     * @todo Special case: scalar not handled yet by
+     * ``OperatorTensor::computeOutputDims()``
+     */
+    // SECTION("Scalar / Scalar") {
+    //     // input_0
+    //     T0->resize({});
+
+    //     // input_1
+    //     T1->resize({});
+
+    //     REQUIRE_NOTHROW(op->computeOutputDims());
+    //     REQUIRE((op->getOutput(0)->dims() == std::vector<std::size_t>()));
+    // }
+    // SECTION("Scalar / +1-D") {
+    //     // a scalar is compatible with any other Tensor
+    //     // input_0
+    //     T0->resize({});
+
+    //     for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+
+    //         // input_1
+    //         const std::size_t nb_dims = nbDimsDist(gen);
+    //         std::vector<std::size_t> dims(nb_dims);
+    //         for (std::size_t i = 0; i < nb_dims; ++i) {
+    //             dims[i] = dimsDist(gen);
+    //         }
+    //         T1->resize(dims);
+
+    //         REQUIRE_NOTHROW(op->computeOutputDims());
+    //         REQUIRE((op->getOutput(0)->dims()) == dims);
+    //     }
+    // }
+    // SECTION("+1-D / Scalar") {
+    //     // a scalar is compatible with any other Tensor
+    //     // input_1
+    //     T1->resize({});
+
+    //     for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+
+    //         // input_0
+    //         const std::size_t nb_dims = nbDimsDist(gen);
+    //         std::vector<std::size_t> dims(nb_dims);
+    //         for (std::size_t i = 0; i < nb_dims; ++i) {
+    //             dims[i] = dimsDist(gen);
+    //         }
+    //         T0->resize(dims);
+
+    //         REQUIRE_NOTHROW(op->computeOutputDims());
+    //         REQUIRE((op->getOutput(0)->dims()) == dims);
+    //     }
+    // }
+    SECTION("+1-D / +1-D") {
+        // same size
+        for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+            const std::size_t nb_dims = nbDimsDist(gen) + 1;
+            std::vector<std::size_t> dims0(nb_dims);
+            for (std::size_t i = 0; i < nb_dims; ++i) {
+                dims0[i] = dimsDist(gen) + 1;
+            }
+
+            T0->resize(dims0);
+            T1->resize(dims0);
+            REQUIRE_NOTHROW(op->computeOutputDims());
+            REQUIRE((op->getOutput(0)->dims()) == dims0);
+        }
+
+        // broadcast
+        for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+            const std::size_t nb_dims = nbDimsDist(gen) + 1;
+            std::vector<std::size_t> dims0(nb_dims);
+            for (std::size_t i = 0; i < nb_dims; ++i) {
+                dims0[i] = dimsDist(gen) + 2;
+            }
+            std::vector<std::size_t> dimsOut = dims0;
+            std::vector<std::size_t> dims1 = dims0;
+            for (std::size_t i = 0; i < nb_dims; ++i) {
+                if (dimsDist(gen) <= 5) {
+                    dims1[i] = 1;
+                }
+            }
+            dims1.erase(dims1.cbegin(), dims1.cbegin() + std::min(nbDimsDist(gen), nb_dims-1));
+
+            T0->resize(dims0);
+            T1->resize(dims1);
+
+            REQUIRE_NOTHROW(op->computeOutputDims());
+            REQUIRE((op->getOutput(0)->dims()) == dimsOut);
+
+            // input_0 - wrong
+            // T1->resize({dims[0] + 1});
+            std::vector<std::size_t> dims1_wrong = dims1;
+            for (std::size_t i = 0; i < dims1.size(); ++i) {
+                ++dims1_wrong[i];
+            }
+            T1->resize(dims1_wrong);
+            REQUIRE(dims0 != dims1_wrong);
+            REQUIRE_THROWS(op->computeOutputDims());
+        }
+    }
+}
+} // namespace Aidge
diff --git a/unit_tests/operator/Test_Sub_Op.cpp b/unit_tests/operator/Test_Sub_Op.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b7b744410d31ea32dea5a15cc7a29da093488d14
--- /dev/null
+++ b/unit_tests/operator/Test_Sub_Op.cpp
@@ -0,0 +1,144 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <catch2/catch_test_macros.hpp>
+#include <cstddef>  // std::size_t
+#include <memory>
+#include <random>   // std::random_device, std::mt19937, std::uniform_int_distribution
+#include <vector>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/operator/Sub.hpp"
+#include "aidge/operator/OperatorTensor.hpp"
+
+namespace Aidge {
+TEST_CASE("[core/operator] Sub_Op(computeOutputDims)", "[Sub][computeOutputDims]") {
+    constexpr std::uint16_t NBTRIALS = 10;
+
+    // Create a random number generator
+    std::random_device rd;
+    std::mt19937 gen(rd());
+    std::uniform_int_distribution<std::size_t> dimsDist(1, 10);
+    std::uniform_int_distribution<std::size_t> nbDimsDist(1, 5);
+
+    // Create Sub Operator
+    std::shared_ptr<Node> mySub = Sub();
+    auto op = std::static_pointer_cast<OperatorTensor>(mySub -> getOperator());
+
+    // input_0
+    std::shared_ptr<Tensor> T0 = std::make_shared<Tensor>();
+    op -> associateInput(0,T0);
+    // input_1
+    std::shared_ptr<Tensor> T1 = std::make_shared<Tensor>();
+    op -> associateInput(1,T1);
+
+    /**
+     * @todo Special case: scalar not handled yet by
+     * ``OperatorTensor::computeOutputDims()``
+     */
+    // SECTION("Scalar / Scalar") {
+    //     // input_0
+    //     T0->resize({});
+
+    //     // input_1
+    //     T1->resize({});
+
+    //     REQUIRE_NOTHROW(op->computeOutputDims());
+    //     REQUIRE((op->getOutput(0)->dims() == std::vector<std::size_t>()));
+    // }
+    // SECTION("Scalar / +1-D") {
+    //     // a scalar is compatible with any other Tensor
+    //     // input_0
+    //     T0->resize({});
+
+    //     for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+
+    //         // input_1
+    //         const std::size_t nb_dims = nbDimsDist(gen);
+    //         std::vector<std::size_t> dims(nb_dims);
+    //         for (std::size_t i = 0; i < nb_dims; ++i) {
+    //             dims[i] = dimsDist(gen);
+    //         }
+    //         T1->resize(dims);
+
+    //         REQUIRE_NOTHROW(op->computeOutputDims());
+    //         REQUIRE((op->getOutput(0)->dims()) == dims);
+    //     }
+    // }
+    // SECTION("+1-D / Scalar") {
+    //     // a scalar is compatible with any other Tensor
+    //     // input_1
+    //     T1->resize({});
+
+    //     for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+
+    //         // input_0
+    //         const std::size_t nb_dims = nbDimsDist(gen);
+    //         std::vector<std::size_t> dims(nb_dims);
+    //         for (std::size_t i = 0; i < nb_dims; ++i) {
+    //             dims[i] = dimsDist(gen);
+    //         }
+    //         T0->resize(dims);
+
+    //         REQUIRE_NOTHROW(op->computeOutputDims());
+    //         REQUIRE((op->getOutput(0)->dims()) == dims);
+    //     }
+    // }
+    SECTION("+1-D / +1-D") {
+        // same size
+        for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+            const std::size_t nb_dims = nbDimsDist(gen) + 1;
+            std::vector<std::size_t> dims0(nb_dims);
+            for (std::size_t i = 0; i < nb_dims; ++i) {
+                dims0[i] = dimsDist(gen) + 1;
+            }
+
+            T0->resize(dims0);
+            T1->resize(dims0);
+            REQUIRE_NOTHROW(op->computeOutputDims());
+            REQUIRE((op->getOutput(0)->dims()) == dims0);
+        }
+
+        // broadcast
+        for (std::uint16_t trial = 0; trial < NBTRIALS; ++trial) {
+            const std::size_t nb_dims = nbDimsDist(gen) + 1;
+            std::vector<std::size_t> dims0(nb_dims);
+            for (std::size_t i = 0; i < nb_dims; ++i) {
+                dims0[i] = dimsDist(gen) + 2;
+            }
+            std::vector<std::size_t> dimsOut = dims0;
+            std::vector<std::size_t> dims1 = dims0;
+            for (std::size_t i = 0; i < nb_dims; ++i) {
+                if (dimsDist(gen) <= 5) {
+                    dims1[i] = 1;
+                }
+            }
+            dims1.erase(dims1.cbegin(), dims1.cbegin() + std::min(nbDimsDist(gen), nb_dims-1));
+
+            T0->resize(dims0);
+            T1->resize(dims1);
+
+            REQUIRE_NOTHROW(op->computeOutputDims());
+            REQUIRE((op->getOutput(0)->dims()) == dimsOut);
+
+            // input_0 - wrong
+            // T1->resize({dims[0] + 1});
+            std::vector<std::size_t> dims1_wrong = dims1;
+            for (std::size_t i = 0; i < dims1.size(); ++i) {
+                ++dims1_wrong[i];
+            }
+            T1->resize(dims1_wrong);
+            REQUIRE(dims0 != dims1_wrong);
+            REQUIRE_THROWS(op->computeOutputDims());
+        }
+    }
+}
+} // namespace Aidge
diff --git a/unit_tests/recipies/Test_FuseMulAdd.cpp b/unit_tests/recipes/Test_FuseMulAdd.cpp
similarity index 89%
rename from unit_tests/recipies/Test_FuseMulAdd.cpp
rename to unit_tests/recipes/Test_FuseMulAdd.cpp
index 968826230dfdf85290ee377aee155e06855c4b28..4c6e3f9d563d2e74958e68f8876a49a8323f4403 100644
--- a/unit_tests/recipies/Test_FuseMulAdd.cpp
+++ b/unit_tests/recipes/Test_FuseMulAdd.cpp
@@ -18,16 +18,16 @@
 #include "aidge/operator/FC.hpp"
 #include "aidge/operator/MatMul.hpp"
 #include "aidge/operator/Producer.hpp"
-#include "aidge/recipies/Recipies.hpp"
+#include "aidge/recipes/Recipes.hpp"
 
 namespace Aidge {
 
 
-TEST_CASE("[cpu/recipies] FuseMulAdd", "[FuseMulAdd][recipies]") {
+TEST_CASE("[cpu/recipes] FuseMulAdd", "[FuseMulAdd][recipes]") {
     // generate the original GraphView
-    auto matmul0 = MatMul(5, 5, "matmul0");
+    auto matmul0 = MatMul("matmul0");
     auto add0 = Add(2, "add0");
-    auto matmul1 = MatMul(5, 5, "matmul1");
+    auto matmul1 = MatMul("matmul1");
     auto add1 = Add(2, "add1");
 
     auto b0 = Producer({5}, "B0");
@@ -49,7 +49,7 @@ TEST_CASE("[cpu/recipies] FuseMulAdd", "[FuseMulAdd][recipies]") {
     b1->addChild(add1, 0, 1);
 
     auto g = std::make_shared<GraphView>();
-    g->add({matmul0, add0, matmul1, add1, b0, b1});
+    g->add({w0, matmul0, b0, add0, w1, matmul1, b1, add1});
 
     // Check original graph
     REQUIRE(g->getNodes() ==
diff --git a/unit_tests/recipies/Test_LabelGraph.cpp b/unit_tests/recipes/Test_LabelGraph.cpp
similarity index 99%
rename from unit_tests/recipies/Test_LabelGraph.cpp
rename to unit_tests/recipes/Test_LabelGraph.cpp
index e0ba9be6c80ef6109b59458bf52a23120efc7584..78f67d823a17454c1ecff40a2307556c990c4f53 100644
--- a/unit_tests/recipies/Test_LabelGraph.cpp
+++ b/unit_tests/recipes/Test_LabelGraph.cpp
@@ -11,7 +11,7 @@
 
 #include <catch2/catch_test_macros.hpp>
 
-#include "aidge/recipies/LabelGraph.hpp"
+#include "aidge/recipes/LabelGraph.hpp"
 #include "aidge/operator/Conv.hpp"
 #include "aidge/operator/AvgPooling.hpp"
 #include "aidge/operator/MaxPooling.hpp"
diff --git a/unit_tests/recipes/Test_removeFlatten.cpp b/unit_tests/recipes/Test_removeFlatten.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..84099ac0b77a633893af6a7550464e539c95d806
--- /dev/null
+++ b/unit_tests/recipes/Test_removeFlatten.cpp
@@ -0,0 +1,101 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <catch2/catch_test_macros.hpp>
+#include <memory>
+#include <set>
+
+#include "aidge/data/Tensor.hpp"
+#include "aidge/graph/GraphView.hpp"
+#include "aidge/graph/OpArgs.hpp"
+#include "aidge/operator/FC.hpp"
+#include "aidge/operator/GenericOperator.hpp"
+#include "aidge/operator/Producer.hpp"
+#include "aidge/recipes/Recipes.hpp"
+#include "aidge/utils/Types.h"
+
+namespace Aidge {
+
+TEST_CASE("[cpu/recipies] RemoveFlatten", "[RemoveFlatten][recipies]") {
+  std::shared_ptr<Node> flatten =
+      GenericOperator("Flatten", 1, 0, 1, "myFlatten");
+  std::shared_ptr<Node> fc0 = FC(10, 10, "FC_1");
+  std::shared_ptr<Node> fc1 = FC(10, 10, "FC_2");
+  std::shared_ptr<Node> prod = Producer(std::array<DimSize_t, 10>(), "myProd");
+
+  SECTION("flatten last layer : nothing removed because pattern searched is "
+          "Flatten=>FC") {
+    std::shared_ptr<Aidge::GraphView> g = Sequential({fc0, flatten});
+
+    removeFlatten(g);
+
+    CHECK(g->getOrderedOutputs().size() == 1);
+    CHECK(g->getOrderedOutputs()[0].first == flatten);
+
+    CHECK(g->getOrderedInputs().size() == 1);
+    CHECK(g->getOrderedInputs()[0].first == fc0);
+    
+    CHECK(fc0->getParent(0) == nullptr);
+    CHECK(fc0->getChildren(0).size() == 1);
+    CHECK(g->rootNode() == fc0);
+  }
+  SECTION("flatten first layer : flatten removed") {
+    auto g = Sequential({flatten, fc0});
+
+    removeFlatten(g);
+
+    CHECK(g->getOrderedInputs().size() == 1);
+    CHECK(g->getOrderedInputs()[0].first == fc0);
+    
+    CHECK(g->getOrderedOutputs().size() == 1);
+    CHECK(g->getOrderedOutputs()[0].first == fc0);
+    
+    CHECK(fc0->getParent(0) == nullptr);
+    CHECK(fc0->getChildren(0).size() == 0);
+    CHECK(g->rootNode() == fc0);
+  }
+  SECTION("flatten middle layer") {
+
+    auto g = Sequential({fc0, flatten, fc1});
+
+    removeFlatten(g);
+
+    CHECK(g->getOrderedInputs().size() == 1);
+    CHECK(g->getOrderedInputs()[0].first == fc0);
+
+    CHECK(g->getOrderedOutputs().size() == 1);
+    CHECK(g->getOrderedOutputs()[0].first == fc1);
+    
+    CHECK(fc1->getParent(0) == fc0);
+    CHECK(fc0->getChildren(0)[0] == fc1);
+    CHECK(g->rootNode() == fc0);
+  }
+  SECTION("flatten right after a producer") {
+    auto g = Sequential({prod, flatten, fc0});
+    // prod->addChild(flatten, 0);
+    // flatten->addChild(fc0, 0);
+    // auto g = std::make_shared<GraphView>({prod, flatten, fc0});
+
+    removeFlatten(g);
+
+    CHECK(g->getOrderedInputs().size() == 0);
+    
+    CHECK(g->getOrderedOutputs().size() == 1);
+    CHECK(g->getOrderedOutputs()[0].first == fc0);
+    
+    CHECK(fc0->getParent(0) == prod);
+    CHECK(fc0->getChildren(0).size() == 0);
+
+    CHECK(g->rootNode() == prod);
+  }
+}
+
+} // namespace Aidge
diff --git a/unit_tests/recipies/Test_removeFlatten.cpp b/unit_tests/recipies/Test_removeFlatten.cpp
deleted file mode 100644
index 8d0ff29dae19ba2dd8009441c39da53bf44378f0..0000000000000000000000000000000000000000
--- a/unit_tests/recipies/Test_removeFlatten.cpp
+++ /dev/null
@@ -1,49 +0,0 @@
-/********************************************************************************
- * Copyright (c) 2023 CEA-List
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * SPDX-License-Identifier: EPL-2.0
- *
- ********************************************************************************/
-
-#include <catch2/catch_test_macros.hpp>
-#include <set>
-
-#include "aidge/data/Tensor.hpp"
-#include "aidge/graph/GraphView.hpp"
-#include "aidge/operator/GenericOperator.hpp"
-#include "aidge/operator/FC.hpp"
-#include "aidge/recipies/Recipies.hpp"
-
-namespace Aidge {
-
-
-TEST_CASE("[cpu/recipies] RemoveFlatten", "[RemoveFlatten][recipies]") {
-    // generate the original GraphView
-    auto flatten = GenericOperator("Flatten", 1, 0, 1, "myFlatten");
-    auto fc = FC(10, 50, "myFC");
-
-    flatten -> addChild(fc);
-
-    auto g = std::make_shared<GraphView>();
-    g->add({fc, flatten});
-
-    // Check original graph
-    // g -> save("before_remove_flatten");
-
-    // use recipie
-    removeFlatten(g);
-
-    // Check transformed graph
-    // g -> save("after_remove_flatten");
-
-    REQUIRE(g->getOrderedInputs().size() == 1);
-    REQUIRE(g->getOrderedOutputs().size() == 1);
-    REQUIRE(g->getOrderedInputs()[0].first == fc);
-    REQUIRE(g->getOrderedOutputs()[0].first == fc);
-}
-
-}  // namespace Aidge
\ No newline at end of file
diff --git a/unit_tests/scheduler/Test_MemoryManager.cpp b/unit_tests/scheduler/Test_MemoryManager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a4941203644b7ba291682f3932926a36fa83b745
--- /dev/null
+++ b/unit_tests/scheduler/Test_MemoryManager.cpp
@@ -0,0 +1,599 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <catch2/catch_test_macros.hpp>
+
+#include "aidge/operator/GenericOperator.hpp"
+#include "aidge/scheduler/MemoryManager.hpp"
+
+using namespace Aidge;
+
+TEST_CASE("allocate1", "[MemoryManager]") {
+    std::shared_ptr<Node> node1
+        = GenericOperator("Fictive", 0, 0, 0, "node1");
+    std::shared_ptr<Node> node2
+        = GenericOperator("Fictive", 0, 0, 0, "node2");
+    std::shared_ptr<Node> node3
+        = GenericOperator("Fictive", 0, 0, 0, "node3");
+    std::shared_ptr<Node> node4
+        = GenericOperator("Fictive", 0, 0, 0, "node4");
+
+    MemoryManager memManager;
+    memManager.allocate(node1, 1024, {node2});
+
+    REQUIRE(memManager.getPeakUsage() == 1024);
+    REQUIRE(memManager.getPlanes(node1).size() == 1);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->offset == 0);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->size == 1024);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node2}));
+    REQUIRE(memManager.getPlanes(node1).back().offset == 0);
+    REQUIRE(memManager.getPlanes(node1).back().size == 1024);
+    REQUIRE(memManager.getPlanes(node1).back().getLimit() == 1024);
+    REQUIRE(memManager.getPlanes(node1).back().count == 1);
+    REQUIRE(memManager.getPlanes(node1).back().length == 1);
+    REQUIRE(memManager.getPlanes(node1).back().stride == 1024);
+
+    memManager.releaseDependencies(node1);
+
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node2}));
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->released == -1);
+
+    memManager.tick();
+    memManager.release(node1);
+
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->released == 1);
+
+    memManager.allocate(node2, 2048, {node3});
+
+    REQUIRE(memManager.getPeakUsage() == 1024 + 2048);
+    REQUIRE(memManager.getPlanes(node2).size() == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->allocated == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->offset == 1024);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node3}));
+    REQUIRE(memManager.getPlanes(node2).back().offset == 0);
+    REQUIRE(memManager.getPlanes(node2).back().size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().getLimit() == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().count == 1);
+    REQUIRE(memManager.getPlanes(node2).back().length == 1);
+    REQUIRE(memManager.getPlanes(node2).back().stride == 2048);
+
+    memManager.releaseDependencies(node2);
+
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node3}));
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->dependencies.empty());
+
+    memManager.tick();
+    memManager.release(node2);
+
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->allocated == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == 2);
+
+    const std::vector<MemoryManager::MemoryPlane>& memPlanes
+        = memManager.getPlanes(node2);
+
+    REQUIRE(memPlanes.size() == 1);
+
+    memManager.reallocate(memPlanes.back().memSpace,
+                          node3, 512, 2048, false, 0, {node4});
+
+    REQUIRE(memManager.getPeakUsage() == 1024 + 2048 + 512);
+    REQUIRE(memManager.getPlanes(node3).size() == 1);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->allocated == 1);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->offset == 1024);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->size == 2048 + 512);
+    REQUIRE(memManager.getPlanes(node3).back().offset == 512);
+    REQUIRE(memManager.getPlanes(node3).back().size == 2048);
+    REQUIRE(memManager.getPlanes(node3).back().getLimit() == 2048);
+    REQUIRE(memManager.getPlanes(node3).back().count == 1);
+    REQUIRE(memManager.getPlanes(node3).back().length == 1);
+    REQUIRE(memManager.getPlanes(node3).back().stride == 2048);
+
+    memManager.releaseDependencies(node3);
+    memManager.tick();
+    memManager.release(node3);
+
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->allocated == 1);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->released == 3);
+
+    memManager.allocate(node4, 1024);
+
+    REQUIRE(memManager.getPeakUsage() == 1024 + 2048 + 512);
+    REQUIRE(memManager.getPlanes(node4).size() == 1);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->allocated == 3);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->offset == 0);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->size == 1024);
+    REQUIRE(memManager.getPlanes(node4).back().offset == 0);
+    REQUIRE(memManager.getPlanes(node4).back().size == 1024);
+    REQUIRE(memManager.getPlanes(node4).back().getLimit() == 1024);
+    REQUIRE(memManager.getPlanes(node4).back().count == 1);
+    REQUIRE(memManager.getPlanes(node4).back().length == 1);
+    REQUIRE(memManager.getPlanes(node4).back().stride == 1024);
+
+    memManager.releaseDependencies(node4);
+    memManager.tick();
+    memManager.release(node4);
+
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->allocated == 3);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->released == 4);
+
+    memManager.log("MemoryManager_allocate1.log");
+}
+
+TEST_CASE("allocate2", "[MemoryManager]") {
+    std::shared_ptr<Node> node1
+        = GenericOperator("Fictive", 0, 0, 0, "node1");
+    std::shared_ptr<Node> node2
+        = GenericOperator("Fictive", 0, 0, 0, "node2");
+    std::shared_ptr<Node> node3
+        = GenericOperator("Fictive", 0, 0, 0, "node3");
+    std::shared_ptr<Node> node4
+        = GenericOperator("Fictive", 0, 0, 0, "node4");
+
+    MemoryManager memManager;
+    memManager.allocate(node1, 1024, {node2});
+
+    REQUIRE(memManager.getPeakUsage() == 1024);
+    REQUIRE(memManager.getPlanes(node1).size() == 1);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->offset == 0);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->size == 1024);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node2}));
+    REQUIRE(memManager.getPlanes(node1).back().offset == 0);
+    REQUIRE(memManager.getPlanes(node1).back().size == 1024);
+    REQUIRE(memManager.getPlanes(node1).back().getLimit() == 1024);
+    REQUIRE(memManager.getPlanes(node1).back().count == 1);
+    REQUIRE(memManager.getPlanes(node1).back().length == 1);
+    REQUIRE(memManager.getPlanes(node1).back().stride == 1024);
+
+    memManager.releaseDependencies(node1);
+
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node2}));
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->released == -1);
+
+    memManager.tick();
+    memManager.release(node1);
+
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->released == 1);
+
+    memManager.allocate(node2, 2048, {node3});
+
+    REQUIRE(memManager.getPeakUsage() == 1024 + 2048);
+    REQUIRE(memManager.getPlanes(node2).size() == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->allocated == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->offset == 1024);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node3}));
+    REQUIRE(memManager.getPlanes(node2).back().offset == 0);
+    REQUIRE(memManager.getPlanes(node2).back().size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().getLimit() == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().count == 1);
+    REQUIRE(memManager.getPlanes(node2).back().length == 1);
+    REQUIRE(memManager.getPlanes(node2).back().stride == 2048);
+
+    memManager.releaseDependencies(node2);
+
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node3}));
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->dependencies.empty());
+
+    memManager.tick();
+    memManager.release(node2);
+
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->allocated == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == 2);
+
+    const std::vector<MemoryManager::MemoryPlane>& memPlanes
+        = memManager.getPlanes(node1);
+
+    REQUIRE(memPlanes.size() == 1);
+
+    memManager.reallocate(memPlanes.back().memSpace,
+                          node3, 512, 2048, false, 0, {node4});
+
+    REQUIRE(memManager.getPeakUsage() == 2048 + 2048 + 512);
+    REQUIRE(memManager.getPlanes(node3).size() == 1);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->offset == 0);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->size == 2048 + 512);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node4}));
+    REQUIRE(memManager.getPlanes(node3).back().offset == 512);
+    REQUIRE(memManager.getPlanes(node3).back().size == 2048);
+    REQUIRE(memManager.getPlanes(node3).back().getLimit() == 2048);
+    REQUIRE(memManager.getPlanes(node3).back().count == 1);
+    REQUIRE(memManager.getPlanes(node3).back().length == 1);
+    REQUIRE(memManager.getPlanes(node3).back().stride == 2048);
+
+    // node2 memSpace should have moved
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->allocated == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == 2);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->offset == 2048 + 512);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node3}));
+    REQUIRE(memManager.getPlanes(node2).back().offset == 0);
+    REQUIRE(memManager.getPlanes(node2).back().size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().getLimit() == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().count == 1);
+    REQUIRE(memManager.getPlanes(node2).back().length == 1);
+    REQUIRE(memManager.getPlanes(node2).back().stride == 2048);
+
+    memManager.releaseDependencies(node3);
+
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node4}));
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->dependencies.empty());
+
+    memManager.tick();
+    memManager.release(node3);
+
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->released == 3);
+
+    memManager.allocate(node4, 1024);
+
+    REQUIRE(memManager.getPeakUsage() == 2048 + 2048 + 512);
+    REQUIRE(memManager.getPlanes(node4).size() == 1);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->allocated == 3);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->offset == 2048 + 512);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->size == 1024);
+    REQUIRE(memManager.getPlanes(node4).back().offset == 0);
+    REQUIRE(memManager.getPlanes(node4).back().size == 1024);
+    REQUIRE(memManager.getPlanes(node4).back().getLimit() == 1024);
+    REQUIRE(memManager.getPlanes(node4).back().count == 1);
+    REQUIRE(memManager.getPlanes(node4).back().length == 1);
+    REQUIRE(memManager.getPlanes(node4).back().stride == 1024);
+
+    memManager.releaseDependencies(node4);
+    memManager.tick();
+    memManager.release(node4);
+
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->allocated == 3);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->released == 4);
+
+    memManager.log("MemoryManager_allocate2.log");
+}
+
+TEST_CASE("allocate3", "[MemoryManager]") {
+    std::shared_ptr<Node> node1
+        = GenericOperator("Fictive", 0, 0, 0, "node1");
+    std::shared_ptr<Node> node2
+        = GenericOperator("Fictive", 0, 0, 0, "node2");
+    std::shared_ptr<Node> node3
+        = GenericOperator("Fictive", 0, 0, 0, "node3");
+    std::shared_ptr<Node> node4
+        = GenericOperator("Fictive", 0, 0, 0, "node4");
+
+    MemoryManager memManager;
+    memManager.allocate(node1, 1024, {node2});
+
+    REQUIRE(memManager.getPeakUsage() == 1024);
+    REQUIRE(memManager.getPlanes(node1).size() == 1);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->offset == 0);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->size == 1024);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node2}));
+    REQUIRE(memManager.getPlanes(node1).back().offset == 0);
+    REQUIRE(memManager.getPlanes(node1).back().size == 1024);
+    REQUIRE(memManager.getPlanes(node1).back().getLimit() == 1024);
+    REQUIRE(memManager.getPlanes(node1).back().count == 1);
+    REQUIRE(memManager.getPlanes(node1).back().length == 1);
+    REQUIRE(memManager.getPlanes(node1).back().stride == 1024);
+
+    memManager.releaseDependencies(node1);
+
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node2}));
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->released == -1);
+
+    memManager.tick();
+    memManager.release(node1);
+
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->released == 1);
+
+    memManager.allocate(node2, 2048, {node3});
+
+    REQUIRE(memManager.getPeakUsage() == 1024 + 2048);
+    REQUIRE(memManager.getPlanes(node2).size() == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->allocated == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->offset == 1024);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node3}));
+    REQUIRE(memManager.getPlanes(node2).back().offset == 0);
+    REQUIRE(memManager.getPlanes(node2).back().size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().getLimit() == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().count == 1);
+    REQUIRE(memManager.getPlanes(node2).back().length == 1);
+    REQUIRE(memManager.getPlanes(node2).back().stride == 2048);
+
+    memManager.releaseDependencies(node2);
+
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node3}));
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->dependencies.empty());
+
+    memManager.tick();
+    memManager.release(node2);
+
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->allocated == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == 2);
+
+    const std::vector<MemoryManager::MemoryPlane>& memPlanes
+        = memManager.getPlanes(node1);
+
+    REQUIRE(memPlanes.size() == 1);
+
+    memManager.reallocate(memPlanes.back().memSpace,
+                          node3, 512, 2048, false);
+
+    REQUIRE(memManager.getPeakUsage() == 2048 + 2048 + 512);
+    REQUIRE(memManager.getPlanes(node3).size() == 1);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->offset == 0);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->size == 2048 + 512);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->dependencies.empty());
+    REQUIRE(memManager.getPlanes(node3).back().offset == 512);
+    REQUIRE(memManager.getPlanes(node3).back().size == 2048);
+    REQUIRE(memManager.getPlanes(node3).back().getLimit() == 2048);
+    REQUIRE(memManager.getPlanes(node3).back().count == 1);
+    REQUIRE(memManager.getPlanes(node3).back().length == 1);
+    REQUIRE(memManager.getPlanes(node3).back().stride == 2048);
+
+    // node2 memSpace should have moved
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->allocated == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == 2);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->offset == 2048 + 512);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node3}));
+    REQUIRE(memManager.getPlanes(node2).back().offset == 0);
+    REQUIRE(memManager.getPlanes(node2).back().size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().getLimit() == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().count == 1);
+    REQUIRE(memManager.getPlanes(node2).back().length == 1);
+    REQUIRE(memManager.getPlanes(node2).back().stride == 2048);
+
+    memManager.releaseDependencies(node3);
+
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->dependencies.empty());
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->dependencies.empty());
+
+    memManager.tick();
+    memManager.release(node3);
+
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->released == 3);
+
+    memManager.reallocate(memPlanes.back().memSpace,
+                          node4, 256, 1024, false);
+
+    REQUIRE(memManager.getPeakUsage() == 2048 + 2048 + 512);
+    REQUIRE(memManager.getPlanes(node4).size() == 1);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->offset == 0);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->size == 2048 + 512);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->dependencies.empty());
+    REQUIRE(memManager.getPlanes(node4).back().offset == 256);
+    REQUIRE(memManager.getPlanes(node4).back().size == 1024);
+    REQUIRE(memManager.getPlanes(node4).back().getLimit() == 2048 + 256);
+    REQUIRE(memManager.getPlanes(node4).back().count == 1);
+    REQUIRE(memManager.getPlanes(node4).back().length == 1);
+    REQUIRE(memManager.getPlanes(node4).back().stride == 1024);
+
+    // node2 memSpace should not have moved
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->allocated == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == 2);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->offset == 2048 + 512);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().offset == 0);
+    REQUIRE(memManager.getPlanes(node2).back().size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().getLimit() == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().count == 1);
+    REQUIRE(memManager.getPlanes(node2).back().length == 1);
+    REQUIRE(memManager.getPlanes(node2).back().stride == 2048);
+
+    memManager.releaseDependencies(node4);
+    memManager.tick();
+    memManager.release(node4);
+
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->released == 4);
+
+    memManager.log("MemoryManager_allocate3.log");
+}
+
+TEST_CASE("allocate3_wrapAround", "[MemoryManager]") {
+    std::shared_ptr<Node> node1
+        = GenericOperator("Fictive", 0, 0, 0, "node1");
+    std::shared_ptr<Node> node2
+        = GenericOperator("Fictive", 0, 0, 0, "node2");
+    std::shared_ptr<Node> node3
+        = GenericOperator("Fictive", 0, 0, 0, "node3");
+    std::shared_ptr<Node> node4
+        = GenericOperator("Fictive", 0, 0, 0, "node4");
+
+    MemoryManager memManager;
+    memManager.allocate(node1, 1024, {node2});
+
+    REQUIRE(memManager.getPeakUsage() == 1024);
+    REQUIRE(memManager.getPlanes(node1).size() == 1);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->offset == 0);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->size == 1024);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node2}));
+    REQUIRE(memManager.getPlanes(node1).back().offset == 0);
+    REQUIRE(memManager.getPlanes(node1).back().size == 1024);
+    REQUIRE(memManager.getPlanes(node1).back().getLimit() == 1024);
+    REQUIRE(memManager.getPlanes(node1).back().count == 1);
+    REQUIRE(memManager.getPlanes(node1).back().length == 1);
+    REQUIRE(memManager.getPlanes(node1).back().stride == 1024);
+
+    memManager.releaseDependencies(node1);
+
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node2}));
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->released == -1);
+
+    memManager.tick();
+    memManager.release(node1);
+
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->released == 1);
+
+    memManager.allocate(node2, 2048, {node3});
+
+    REQUIRE(memManager.getPeakUsage() == 1024 + 2048);
+    REQUIRE(memManager.getPlanes(node2).size() == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->allocated == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->offset == 1024);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node3}));
+    REQUIRE(memManager.getPlanes(node2).back().offset == 0);
+    REQUIRE(memManager.getPlanes(node2).back().size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().getLimit() == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().count == 1);
+    REQUIRE(memManager.getPlanes(node2).back().length == 1);
+    REQUIRE(memManager.getPlanes(node2).back().stride == 2048);
+
+    memManager.releaseDependencies(node2);
+
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node3}));
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node1).back().memSpace->dependencies.empty());
+
+    memManager.tick();
+    memManager.release(node2);
+
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->allocated == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == 2);
+
+    const std::vector<MemoryManager::MemoryPlane>& memPlanes
+        = memManager.getPlanes(node1);
+
+    REQUIRE(memPlanes.size() == 1);
+
+    memManager.reallocate(memPlanes.back().memSpace,
+                          node3, 512, 2048, true);
+
+    REQUIRE(memManager.getPeakUsage() == 2048 + 2048);
+    REQUIRE(memManager.getPlanes(node3).size() == 1);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->offset == 0);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->size == 2048);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->dependencies.empty());
+    REQUIRE(memManager.getPlanes(node3).back().offset == 512);
+    REQUIRE(memManager.getPlanes(node3).back().size == 2048);
+    REQUIRE(memManager.getPlanes(node3).back().getLimit() == 2048 - 512);
+    REQUIRE(memManager.getPlanes(node3).back().count == 1);
+    REQUIRE(memManager.getPlanes(node3).back().length == 1);
+    REQUIRE(memManager.getPlanes(node3).back().stride == 2048);
+
+    // node2 memSpace should have moved
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->allocated == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == 2);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->offset == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->dependencies ==
+        std::set<std::shared_ptr<Node> >({node3}));
+    REQUIRE(memManager.getPlanes(node2).back().offset == 0);
+    REQUIRE(memManager.getPlanes(node2).back().size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().getLimit() == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().count == 1);
+    REQUIRE(memManager.getPlanes(node2).back().length == 1);
+    REQUIRE(memManager.getPlanes(node2).back().stride == 2048);
+
+    memManager.releaseDependencies(node3);
+
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->dependencies.empty());
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->dependencies.empty());
+
+    memManager.tick();
+    memManager.release(node3);
+
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node3).back().memSpace->released == 3);
+
+    memManager.reallocate(memPlanes.back().memSpace,
+                          node4, 1024, 1792, true);
+
+    REQUIRE(memManager.getPeakUsage() == 2048 + 2048);
+    REQUIRE(memManager.getPlanes(node4).size() == 1);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->released == -1);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->offset == 0);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->size == 2048);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->dependencies.empty());
+    REQUIRE(memManager.getPlanes(node4).back().offset == 1024);
+    REQUIRE(memManager.getPlanes(node4).back().size == 1792);
+    REQUIRE(memManager.getPlanes(node4).back().getLimit() == 2048 - 1024);
+    REQUIRE(memManager.getPlanes(node4).back().count == 1);
+    REQUIRE(memManager.getPlanes(node4).back().length == 1);
+    REQUIRE(memManager.getPlanes(node4).back().stride == 1792);
+
+    // node2 memSpace should not have moved
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->allocated == 1);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->released == 2);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->offset == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().memSpace->size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().offset == 0);
+    REQUIRE(memManager.getPlanes(node2).back().size == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().getLimit() == 2048);
+    REQUIRE(memManager.getPlanes(node2).back().count == 1);
+    REQUIRE(memManager.getPlanes(node2).back().length == 1);
+    REQUIRE(memManager.getPlanes(node2).back().stride == 2048);
+
+    memManager.releaseDependencies(node4);
+    memManager.tick();
+    memManager.release(node4);
+
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->allocated == 0);
+    REQUIRE(memManager.getPlanes(node4).back().memSpace->released == 4);
+
+    memManager.log("MemoryManager_allocate3_wrapAround.log");
+}
diff --git a/unit_tests/scheduler/Test_Scheduler.cpp b/unit_tests/scheduler/Test_Scheduler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e2c1a8fcb96256fa8c3f26a3495913bd987de2d4
--- /dev/null
+++ b/unit_tests/scheduler/Test_Scheduler.cpp
@@ -0,0 +1,136 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <algorithm> // std::sort
+#include <cassert>
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+
+#include <catch2/catch_test_macros.hpp>
+
+#include "aidge/backend/OperatorImpl.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/graph/GraphView.hpp"
+#include "aidge/graph/OpArgs.hpp"
+#include "aidge/graph/Testing.hpp"
+#include "aidge/operator/GenericOperator.hpp"
+#include "aidge/operator/Producer.hpp"
+#include "aidge/scheduler/SequentialScheduler.hpp"
+
+namespace Aidge {
+
+TEST_CASE("randomScheduling", "[Scheduler][randomGen]") {
+  const size_t nbTests = 10;
+  size_t nbUnicity = 0;
+  std::uniform_int_distribution<std::size_t> nb_nodes_dist(100, 500);
+
+  for (int test = 0; test < nbTests; ++test) {
+    std::random_device rd;
+    const std::mt19937::result_type seed(rd());
+    std::mt19937 gen(rd());
+
+    RandomGraph randGraph;
+    const auto g1 = std::make_shared<GraphView>("g1");
+    const size_t nb_nodes = nb_nodes_dist(gen);
+
+    SECTION("Acyclic Graph") {
+        Aidge::Log::setConsoleLevel(Aidge::Log::Warn);
+      fmt::print("gen acyclic graph of {} nodes...\n", nb_nodes);
+      randGraph.acyclic = true;
+
+      const bool unicity1 = g1->add(randGraph.gen(seed, nb_nodes));
+      // g1->save("test_graph_" + std::to_string(test));
+
+      if (unicity1) {
+        for (auto &node : g1->getNodes()) {
+          std::static_pointer_cast<GenericOperator_Op>(node->getOperator())
+              ->setComputeOutputDims(
+                  GenericOperator_Op::InputIdentity(0, node->nbOutputs()));
+        }
+
+        const auto orderedInputs = g1->getOrderedInputs();
+        for (const auto &input : orderedInputs) {
+          auto prod = Producer({16, 32});
+          prod->addChild(input.first, 0, input.second);
+          g1->add(prod);
+        }
+
+        g1->save("schedule");
+        g1->compile();
+
+        fmt::print("gen scheduling...\n");
+        auto scheduler = SequentialScheduler(g1);
+        scheduler.generateScheduling();
+        fmt::print("gen scheduling finished\n");
+        const auto sch = scheduler.getStaticScheduling();
+
+        const auto namePtrTable = g1->getRankedNodesName("{0} ({1}#{3})");
+
+        std::vector<std::string> nodesName;
+        std::transform(
+            sch.begin(), sch.end(), std::back_inserter(nodesName),
+            [&namePtrTable](auto val) { return namePtrTable.at(val); });
+
+        fmt::print("schedule: {}\n", nodesName);
+        REQUIRE(sch.size() == nb_nodes + orderedInputs.size());
+        ++nbUnicity;
+      }
+    }
+    // SECTION("Cyclic graph") {
+    //   fmt::print("gen cyclic graph of {} nodes...\n", nb_nodes);
+    //   randGraph.acyclic = false;
+    //   randGraph.types={"Memorize"};
+
+    //   const bool unicity1 = g1->add(randGraph.gen(seed, nb_nodes));
+    //   // g1->save("test_graph_" + std::to_string(test));
+
+    //   if (unicity1) {
+    //     for (auto &node : g1->getNodes()) {
+    //       std::static_pointer_cast<GenericOperator_Op>(node->getOperator())
+    //           ->setComputeOutputDims(
+    //               GenericOperator_Op::InputIdentity(0, node->nbOutputs()));
+    //     }
+
+    //     const auto orderedInputs = g1->getOrderedInputs();
+    //     for (const auto &input : orderedInputs) {
+    //       auto prod = Producer({16, 32});
+    //       prod->addChild(input.first, 0, input.second);
+    //       g1->add(prod);
+    //     }
+
+    //     g1->save("schedule");
+    //     g1->forwardDims();
+
+    //     fmt::print("gen scheduling...\n");
+    //     auto scheduler = SequentialScheduler(g1);
+    //     scheduler.generateScheduling();
+    //     fmt::print("gen scheduling finished\n");
+    //     const auto sch = scheduler.getStaticScheduling();
+
+    //     const auto namePtrTable = g1->getRankedNodesName("{0} ({1}#{3})");
+
+    //     std::vector<std::string> nodesName;
+    //     std::transform(
+    //         sch.begin(), sch.end(), std::back_inserter(nodesName),
+    //         [&namePtrTable](auto val) { return namePtrTable.at(val); });
+
+    //     fmt::print("schedule: {}\n", nodesName);
+    //     REQUIRE(sch.size() == nb_nodes + orderedInputs.size());
+    //     ++nbUnicity;
+    //   }
+    // }
+  }
+  fmt::print("nbUnicity = {}/{}\n", nbUnicity, nbTests);
+}
+
+} // namespace Aidge
diff --git a/unit_tests/utils/Test_Log.cpp b/unit_tests/utils/Test_Log.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3d8e672b84f5055a12185c3684c34bd888f0545b
--- /dev/null
+++ b/unit_tests/utils/Test_Log.cpp
@@ -0,0 +1,31 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <catch2/catch_test_macros.hpp>
+
+#include "aidge/utils/Log.hpp"
+
+#include <fmt/color.h>
+
+using namespace Aidge;
+
+TEST_CASE("[core/log] Log") {
+    SECTION("TestLog") {
+        Log::setConsoleLevel(Log::Debug);
+        Log::debug("debug");
+        Log::debug("{}", fmt::styled("green debug", fmt::fg(fmt::color::green)));
+        Log::info("info");
+        Log::notice("notice");
+        Log::warn("warn");
+        Log::error("error");
+        Log::fatal("fatal");
+    }
+}
diff --git a/version.txt b/version.txt
index 17e51c385ea382d4f2ef124b7032c1604845622d..0ea3a944b399d25f7e1b8fe684d754eb8da9fe7f 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-0.1.1
+0.2.0