diff --git a/.gitlab/ci/build.gitlab-ci.yml b/.gitlab/ci/build.gitlab-ci.yml
index 73b85c8a409e675c849b9ca66557c63b5acf6359..cd56a55fa7e9cbcefba4715188fd270462e81976 100644
--- a/.gitlab/ci/build.gitlab-ci.yml
+++ b/.gitlab/ci/build.gitlab-ci.yml
@@ -27,6 +27,8 @@ build:ubuntu_python:
     - python3 -m pip install virtualenv
     - virtualenv venv
     - source venv/bin/activate
+    # Numpy dependancy for unit test
+    - python3 -m pip install numpy
     - export AIDGE_INSTALL=`pwd`/install
     - python3 -m pip install .
   artifacts:
diff --git a/aidge_core/unit_tests/test_parameters.py b/aidge_core/unit_tests/test_parameters.py
index 02c7598820d2429bc49ff9a2f02c8ee841783173..1e24276745312d4483c268156963e0efe413b46c 100644
--- a/aidge_core/unit_tests/test_parameters.py
+++ b/aidge_core/unit_tests/test_parameters.py
@@ -40,7 +40,7 @@ class test_parameters(unittest.TestCase):
 
     def test_matmul(self):
         out_channels = 8
-        matmul_op = aidge_core.Matmul(out_channels).get_operator()
+        matmul_op = aidge_core.MatMul(out_channels).get_operator()
         self.assertEqual(matmul_op.get("OutChannels"), out_channels)
 
     def test_producer_1D(self):
diff --git a/aidge_core/unit_tests/test_recipies.py b/aidge_core/unit_tests/test_recipies.py
new file mode 100644
index 0000000000000000000000000000000000000000..754907443530f7e73d1e10ed9549d0c8eb78a011
--- /dev/null
+++ b/aidge_core/unit_tests/test_recipies.py
@@ -0,0 +1,78 @@
+"""
+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
+
+class test_recipies(unittest.TestCase):
+    """
+    """
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_remove_flatten(self):
+        graph_view = aidge_core.sequential([
+            aidge_core.GenericOperator("Flatten", 1, 1, 1, name="Flatten0"),
+            aidge_core.FC(50, name='0')
+        ])
+        old_nodes = graph_view.get_nodes()
+        aidge_core.remove_flatten(graph_view)
+        self.assertTrue(len(graph_view.get_nodes()) == len(old_nodes) - 1)
+        self.assertTrue("Flatten0" not in [i.name for i in graph_view.get_nodes()])
+
+        self.assertTrue(all([i in old_nodes for i in graph_view.get_nodes()]))
+
+    def test_fuse_matmul_add(self):
+        matmul0 = aidge_core.GenericOperator("MatMul", 1, 2, 1, name="MatMul0")
+        add0 = aidge_core.Add(name="Add0")
+        matmul1 = aidge_core.GenericOperator("MatMul", 1, 2, 1, name="MatMul1")
+        add1 = aidge_core.Add(name="Add1")
+
+        graph_view = aidge_core.sequential([matmul0, add0, matmul1, add1])
+
+        w0 = aidge_core.Producer([1, 1], name="W0")
+        w0.add_child(matmul0, 0, 1)
+        graph_view.add(w0)
+
+        b0 = aidge_core.Producer([1], name="B0")
+        b0.add_child(add0, 0, 1)
+        graph_view.add(b0)
+
+        w1 = aidge_core.Producer([1, 1], name="W1")
+        w1.add_child(matmul1, 0, 1)
+        graph_view.add(w1)
+
+        b1 = aidge_core.Producer([1], name="B1")
+        b1.add_child(add1, 0, 1)
+        graph_view.add(b1)
+
+        old_nodes = graph_view.get_nodes()
+        aidge_core.fuse_mul_add(graph_view)
+
+        self.assertTrue(len(graph_view.get_nodes()) == len(old_nodes) - 2)
+        self.assertTrue("MatMul0" not in [i.name() for i in graph_view.get_nodes()])
+        self.assertTrue("Add0" not in [i.name() for i in graph_view.get_nodes()])
+        self.assertTrue("MatMul1" not in [i.name() for i in graph_view.get_nodes()])
+        self.assertTrue("Add1" not in [i.name() for i in graph_view.get_nodes()])
+
+        self.assertTrue("W0" in [i.name() for i in graph_view.get_nodes()])
+        self.assertTrue("B0" in [i.name() for i in graph_view.get_nodes()])
+        self.assertTrue("W1" in [i.name() for i in graph_view.get_nodes()])
+        self.assertTrue("B1" in [i.name() for i in graph_view.get_nodes()])
+        # TODO : Vérifier que FC bien crée
+
+if __name__ == '__main__':
+    unittest.main()
+
+
+
diff --git a/aidge_core/unit_tests/test_tensor.py b/aidge_core/unit_tests/test_tensor.py
new file mode 100644
index 0000000000000000000000000000000000000000..a214a0e354c64b515d0a7ac24d81c85e116938ca
--- /dev/null
+++ b/aidge_core/unit_tests/test_tensor.py
@@ -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
+"""
+
+import unittest
+import aidge_core
+
+from functools import reduce
+import numpy as np
+
+class test_tensor(unittest.TestCase):
+    """
+    """
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_getcoord_getidx(self):
+        dims = [2,2,2]
+        size = reduce((lambda x, y: x*y), dims)
+
+        np_array = np.arange(size).reshape(dims)
+
+        t = aidge_core.Tensor(np_array)
+        for i in range(size):
+            coord = t.get_coord(i)
+            idx = t.get_idx(coord)
+            self.assertEqual(idx, i)
+
+if __name__ == '__main__':
+    unittest.main()
+
+
+
+
+
diff --git a/include/aidge/aidge.hpp b/include/aidge/aidge.hpp
index cfda3ac7fa024f8cf80b4589d978b9b5bff5b4f0..9c0d8c0b321892d60f40d52eb2a44d4d0fec3a2c 100644
--- a/include/aidge/aidge.hpp
+++ b/include/aidge/aidge.hpp
@@ -33,7 +33,7 @@
 #include "aidge/operator/ConvDepthWise.hpp"
 #include "aidge/operator/FC.hpp"
 #include "aidge/operator/GenericOperator.hpp"
-#include "aidge/operator/Matmul.hpp"
+#include "aidge/operator/MatMul.hpp"
 #include "aidge/operator/MaxPooling.hpp"
 //#include "aidge/operator/MetaOperator.hpp"
 #include "aidge/operator/Operator.hpp"
diff --git a/include/aidge/backend/TensorImpl.hpp b/include/aidge/backend/TensorImpl.hpp
index c56f66fc0b827ccccd9749b9880507dbf48c8179..dfe3d932ac68929acfd26ecf7126e07c4707bcfc 100644
--- a/include/aidge/backend/TensorImpl.hpp
+++ b/include/aidge/backend/TensorImpl.hpp
@@ -27,6 +27,9 @@ public:
     {
         printf("Cannot set raw pointer for backend %s\n", mBackend);
     };
+
+    virtual void* getRaw(std::size_t /*idx*/)=0;
+
     virtual std::size_t scalarSize() const = 0; // Size of one scalar (in bytes)
     constexpr const char *backend() const { return mBackend; }
     virtual ~TensorImpl() = default;
diff --git a/include/aidge/data/Tensor.hpp b/include/aidge/data/Tensor.hpp
index c3a6e478f8943253a9f9b3565db2d4452a9ca133..7422a52eb171ee6dae0e14ad67c0562295fe5d8c 100644
--- a/include/aidge/data/Tensor.hpp
+++ b/include/aidge/data/Tensor.hpp
@@ -446,18 +446,33 @@ class Tensor : public Data,
      */
     bool empty() const { return mDims.empty(); }
 
-    template <typename expectedType, std::array<std::size_t, 1>::size_type DIM>
-    constexpr expectedType &get(std::array<std::size_t, DIM> idx) {
-        assert(DIM == mDims.size());
-        assert(mImpl);
-        std::size_t unfoldedIdx = 0;
-        for (std::size_t i = 0; i < DIM - std::size_t(1); ++i) {
-            unfoldedIdx = (unfoldedIdx + idx[i]) * mDims[i + 1];
-        }
-        unfoldedIdx += idx[DIM - 1];
-        return static_cast<expectedType *>(mImpl->rawPtr())[unfoldedIdx];
+    template <typename expectedType>
+    expectedType& get(std::size_t idx){
+        // TODO : add assert expected Type compatible with datatype
+        // TODO : add assert idx < Size
+        return *reinterpret_cast<expectedType *>(mImpl->getRaw(idx));
+    }
+
+    template <typename expectedType>
+    expectedType& get(std::vector<std::size_t> coordIdx){
+        return get<expectedType>(getIdx(coordIdx));
+    }
+
+    template <typename expectedType>
+    void set(std::size_t idx, expectedType value){
+        // TODO : add assert expected Type compatible with datatype
+        // TODO : add assert idx < Size
+        void* dataPtr = mImpl->getRaw(idx);
+        std::memcpy(dataPtr, &value, sizeof(expectedType));
     }
 
+    template <typename expectedType>
+    void set(std::vector<std::size_t> coordIdx, expectedType value){
+        set<expectedType>(getIdx(coordIdx), value);
+    }
+
+
+
     std::string toString() {
         if (dims().empty()) { return "{}"; }
         std::string res;
@@ -559,6 +574,42 @@ class Tensor : public Data,
         return mGrad;
     }
 
+    /**
+     * @brief From the the 1D index, return the coordinate of an element in the tensor.
+     *
+     * @param flatIdx 1D index of the value considering a flatten 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];
+        }
+        coordIdx[0] = idx % mDims[0];
+        return coordIdx;
+    }
+
+    /**
+     * @brief From the coordinate returns the 1D index of an element in the tensor.
+     *
+     * @param coordIdx Coordinate to an element in the tensor
+     * @return DimSize_t
+     */
+    std::size_t getIdx(std::vector<std::size_t> coordIdx) const {
+        // std::size_t flatIdx = 0;
+        // std::size_t stride = 1;
+        std::size_t flatIdx = 0;
+        assert(coordIdx.size() == mDims.size() && "Coordinates does not match number of dimensions");
+        std::size_t i = 0;
+        for(; i < mDims.size() - 1; ++i){
+            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];
+    }
+
 private:
     ///\bug not protected against overflow
     std::size_t computeSize() {
diff --git a/include/aidge/operator/BatchNorm.hpp b/include/aidge/operator/BatchNorm.hpp
index f1a6ae8f52141839f72211f23511a0607e2138b6..c95ecac92d14be6d56edd7abda6c20b011e65aba 100644
--- a/include/aidge/operator/BatchNorm.hpp
+++ b/include/aidge/operator/BatchNorm.hpp
@@ -97,7 +97,6 @@ public:
         if (!mInputs[0]->empty()) {
             for (std::size_t i = nbDataInputs(); i < nbInputs(); ++i) {
                 if(mInputs[i]->size() != mInputs[0]->dims()[1]) {
-                    assert(!mInputs[0]->hasImpl() && "Incompatible size with already implemented learnable parameter");
                     mInputs[i]->resize(std::array<DimSize_t, 1>({mInputs[0]->dims()[1]}));
                 }
             }
@@ -181,4 +180,4 @@ template <>
 const char *const EnumStrings<Aidge::BatchNormParam>::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/Conv.hpp b/include/aidge/operator/Conv.hpp
index e95b46ae5583df9e6b471dc4005d0d9c4636ca9b..7113adb41d051d67e22456bbac05c00aa15333ab 100644
--- a/include/aidge/operator/Conv.hpp
+++ b/include/aidge/operator/Conv.hpp
@@ -216,8 +216,14 @@ inline std::shared_ptr<Node> Conv(
 
 namespace {
 template <>
-const char *const EnumStrings<Aidge::ConvParam>::data[] = {"StrideDims", "DilationDims", "InChannels", "OutChannels",
-                                                          "KernelDims", "PaddingDims"};
+const char *const EnumStrings<Aidge::ConvParam>::data[] = {
+    "StrideDims",
+    "DilationDims",
+    "InChannels",
+    "OutChannels",
+    "KernelDims",
+    "PaddingDims"
+};
 }
 
 #endif /* AIDGE_CORE_OPERATOR_CONV_H_ */
diff --git a/include/aidge/operator/Matmul.hpp b/include/aidge/operator/MatMul.hpp
similarity index 77%
rename from include/aidge/operator/Matmul.hpp
rename to include/aidge/operator/MatMul.hpp
index 54bbcb267f346fd79a2b9e3a8aca571ed2e6ba91..9e7e7e43a52afdc93a0f3a0430ae64010abb11dc 100644
--- a/include/aidge/operator/Matmul.hpp
+++ b/include/aidge/operator/MatMul.hpp
@@ -27,29 +27,29 @@
 #include "aidge/utils/Registrar.hpp"
 
 namespace Aidge {
-enum class MatmulParam { OutChannels };
+enum class MatMulParam { OutChannels };
 
-class Matmul_Op : public Operator,
-              public Registrable<Matmul_Op,
+class MatMul_Op : public Operator,
+              public Registrable<MatMul_Op,
                                  std::string,
-                                 std::unique_ptr<OperatorImpl>(const Matmul_Op &)>,
-              public Parameterizable<MatmulParam, DimSize_t> {
+                                 std::unique_ptr<OperatorImpl>(const MatMul_Op &)>,
+              public Parameterizable<MatMulParam, DimSize_t> {
 public:
     std::array<std::shared_ptr<Tensor>, 2> mInputs = {std::make_shared<Tensor>(), std::make_shared<Tensor>()};
     const std::shared_ptr<Tensor> mOutput = std::make_shared<Tensor>();
 
 public:
-    static constexpr const char* Type = "Matmul";
+    static constexpr const char* Type = "MatMul";
 
-    Matmul_Op() = delete;
+    MatMul_Op() = delete;
 
-    using Parameterizable_ = Parameterizable<MatmulParam, DimSize_t>;
-    template <MatmulParam e> using param = typename Parameterizable_::template param<e>;
+    using Parameterizable_ = Parameterizable<MatMulParam, DimSize_t>;
+    template <MatMulParam e> using param = typename Parameterizable_::template param<e>;
 
-    Matmul_Op(DimSize_t out_channels)
+    MatMul_Op(DimSize_t out_channels)
             : Operator(Type),
             Parameterizable_(
-                param<MatmulParam::OutChannels>(out_channels))
+                param<MatMulParam::OutChannels>(out_channels))
     {
         setDatatype(DataType::Float32);
     }
@@ -58,22 +58,22 @@ public:
      * @brief Copy-constructor. Copy the operator parameters 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)
+    MatMul_Op(const MatMul_Op& op)
         : Operator(Type),
           Parameterizable_(op),
           mOutput(std::make_shared<Tensor>(*op.mOutput))
     {
         // cpy-ctor
         setDatatype(op.mOutput->dataType());
-        mImpl = op.mImpl ? Registrar<Matmul_Op>::create(mOutput->getImpl()->backend())(*this) : nullptr;
+        mImpl = op.mImpl ? Registrar<MatMul_Op>::create(mOutput->getImpl()->backend())(*this) : nullptr;
     }
 
     /**
      * @brief Clone the operator using its copy-constructor.
-     * @see Operator::Matmul_Op
+     * @see Operator::MatMul_Op
      */
     std::shared_ptr<Operator> clone() const override {
-        return std::make_shared<Matmul_Op>(*this);
+        return std::make_shared<MatMul_Op>(*this);
     }
 
     void associateInput(const IOIndex_t inputIdx, std::shared_ptr<Data> data) override final {
@@ -85,9 +85,9 @@ public:
     void computeOutputDims() override final {
         if (!mInputs[0]->empty()) {
             // <in_features**, out_channels>
-            std::array<DimSize_t, 2> weightDims = {static_cast<DimSize_t>(mInputs[0]->size()), this->template get<MatmulParam::OutChannels>()};
+            std::array<DimSize_t, 2> weightDims = {this->template get<MatMulParam::OutChannels>(), static_cast<DimSize_t>(mInputs[0]->sizeM1())};
             // <out_channels, batch>
-            std::array<DimSize_t, 1> outputDims = {this->template get<MatmulParam::OutChannels>()};
+            std::array<DimSize_t, 2> outputDims = {mInputs[0]->dims()[0], this->template get<MatMulParam::OutChannels>()};
 
             mInputs[1]->resize(weightDims);
             mOutput->resize(outputDims);
@@ -128,7 +128,7 @@ public:
 
 
     void setBackend(const std::string& name) {
-        mImpl = Registrar<Matmul_Op>::create(name)(*this);
+        mImpl = Registrar<MatMul_Op>::create(name)(*this);
         mOutput->setBackend(name);
 
         // FIXME: temporary workaround
@@ -150,17 +150,17 @@ public:
     inline IOIndex_t nbOutputs() const noexcept override final { return 1; }
 };
 
-inline std::shared_ptr<Node> Matmul(DimSize_t out_channels, const std::string& name = "") {
-    // FIXME: properly handle default w&b initialization in every cases
-    auto matmul = std::make_shared<Node>(std::make_shared<Matmul_Op>(out_channels), name);
-    addProducer(matmul, 1, {1, out_channels}, "w");
+inline std::shared_ptr<Node> MatMul(DimSize_t out_channels, const std::string& name = "") {
+    // FIXME: properly handle default w initialization in every cases
+    auto matmul = std::make_shared<Node>(std::make_shared<MatMul_Op>(out_channels), name);
+    addProducer(matmul, 1, {out_channels, 1}, "w");
     return matmul;
 }
 } // namespace Aidge
 
 namespace {
 template <>
-const char *const EnumStrings<Aidge::MatmulParam>::data[] = {"OutChannels"};
+const char *const EnumStrings<Aidge::MatMulParam>::data[] = {"OutChannels"};
 }
 
 #endif /* AIDGE_CORE_OPERATOR__MATMUL_H_ */
diff --git a/include/aidge/operator/MetaOperator.hpp b/include/aidge/operator/MetaOperator.hpp
index 9e12b159888923cfea10dd02b7b267a46abcb3b7..327361de4fc278efbf6cc1afe4c140c7994fe61e 100644
--- a/include/aidge/operator/MetaOperator.hpp
+++ b/include/aidge/operator/MetaOperator.hpp
@@ -34,7 +34,7 @@ public:
 
     /**
      * @brief Clone the operator using its copy-constructor.
-     * @see Operator::Matmul_Op
+     * @see Operator::MatMul_Op
      */
     std::shared_ptr<Operator> clone() const override {
         return std::make_shared<MetaOperator>(*this);
diff --git a/include/aidge/operator/Producer.hpp b/include/aidge/operator/Producer.hpp
index fbab24a0d23712b138c41e969372701fdb3d749e..681ed96892e216094f9392df01f5a10f66609638 100644
--- a/include/aidge/operator/Producer.hpp
+++ b/include/aidge/operator/Producer.hpp
@@ -75,6 +75,16 @@ public:
         assert(false && "Producer operator takes no input");
     }
 
+    /**
+     * @brief Set the Output Tensor of the Producer operator.
+     * This method will create a copy of the Tensor.
+     *
+     * @param newOutput Tensor containing the values to copy 
+     */
+    void setOutputTensor(const Tensor& newOutput) {
+        *mOutput = newOutput;
+    }
+
     void computeOutputDims() override final {}
 
     bool outputDimsForwarded() const override final {return true;}
@@ -163,4 +173,4 @@ void addProducer(std::shared_ptr<Node>& otherNode, const IOIndex_t inputIdx, Dim
 }
 } // namespace Aidge
 
-#endif /* AIDGE_CORE_OPERATOR_PRODUCER_H_ */
\ No newline at end of file
+#endif /* AIDGE_CORE_OPERATOR_PRODUCER_H_ */
diff --git a/include/aidge/utils/Parameter.hpp b/include/aidge/utils/Parameter.hpp
index a475576170915182e25dbaa193ca8a7a3853c0e0..2b48e833533da5b8bb4a5f4f134860e89717804a 100644
--- a/include/aidge/utils/Parameter.hpp
+++ b/include/aidge/utils/Parameter.hpp
@@ -145,11 +145,11 @@ public:
         assert(false && "parameter not found");
     }
 
-    template <typename R, std::size_t SIZE = std::tuple_size<std::tuple<T...>>::value-1>
+    template <typename R, std::size_t SIZE = std::tuple_size<std::tuple<T...>>::value>
     constexpr typename std::enable_if<(SIZE > 0), R&>::type get(std::size_t i) {
-        if (i == SIZE) {
-            if (std::is_same<R, typename std::tuple_element<SIZE,std::tuple<T...>>::type>::value) {
-                return reinterpret_cast<R&>(std::get<SIZE>(mParams));
+        if (i == SIZE-1) {
+            if (std::is_same<R, typename std::tuple_element<SIZE-1,std::tuple<T...>>::type>::value) {
+                return reinterpret_cast<R&>(std::get<SIZE-1>(mParams));
             }
             else {
                 assert(false && "wrong parameter type");
@@ -160,9 +160,10 @@ public:
         }
     }
 
-    template <typename R, std::size_t SIZE = std::tuple_size<std::tuple<T...>>::value-1>
-    constexpr typename std::enable_if<(SIZE <= 0), R&>::type get(std::size_t i) {
+    template <typename R, std::size_t SIZE = std::tuple_size<std::tuple<T...>>::value>
+    [[noreturn]] constexpr typename std::enable_if<(SIZE == 0), R&>::type get(std::size_t /*i*/) {
         assert(false && "parameter not found");
+        exit(-1);
     }
 
     constexpr const std::tuple<T...>& getParams() const {
diff --git a/include/aidge/utils/Recipies.hpp b/include/aidge/utils/Recipies.hpp
index 4cbf8fd284bef314dbe28b19ebdae05172467bad..894e56fae2e9c2f6bcf11e4e76a433f5c8058080 100644
--- a/include/aidge/utils/Recipies.hpp
+++ b/include/aidge/utils/Recipies.hpp
@@ -17,11 +17,54 @@
 
 namespace Aidge{
 
+// FUSE MATMUL + ADD -> FC
+
+/**
+ * @brief Merge ``MatMul`` and :cpp:function:`Aidge::Add` Node into a :cpp:function:`Aidge::FC` Node.
+ *
+ * @param nodes Strict set of Node to merge.
+ */
 void fuseMulAdd(std::set<std::shared_ptr<Node>> nodes);
+/**
+ * @brief Merge ``MatMul`` and :cpp:function:`Aidge::Add` Node into a :cpp:function:`Aidge::FC` Node.
+ *
+ * @param graphView Graph view to use graph matching on, in order to apply transfomrations.
+ */
+void fuseMulAdd(std::shared_ptr<GraphView> graphView);
+
+
+// REMOVE FLATTEN + FC -> FC
+
+/**
+ * @brief Remove ``Flatten`` before :cpp:function:`Aidge::FC` Node.
+ *
+ * @param nodes Strict set of Node to merge.
+ */
 void removeFlatten(std::set<std::shared_ptr<Node>> nodes);
+/**
+ * @brief Remove ``Flatten`` before :cpp:function:`Aidge::FC` Node.
+ *
+ * @param graphView Graph view to use graph matching on, in order to apply transfomrations.
+ */
+void removeFlatten(std::shared_ptr<GraphView> graphView);
+ 
+// FUSE BN + FC || CONV -> FC || CONV
 
+/**
+ * @brief Fuse :cpp:function:`Aidge::BatchNorm` with :cpp:function:`Aidge::Conv` or :cpp:function:`Aidge::FC` Nodes.
+ * Ref: https://nenadmarkus.com/p/fusing-batchnorm-and-conv/
+ *
+ * @param nodes Strict set of Node to merge.
+ */
+void fuseBatchNorm(std::set<std::shared_ptr<Node>> nodes);
+/**
+ * @brief Fuse :cpp:function:`Aidge::BatchNorm` with :cpp:function:`Aidge::Conv` or :cpp:function:`Aidge::FC` Nodes.
+ * Ref: https://nenadmarkus.com/p/fusing-batchnorm-and-conv/
+ *
+ * @param graphView Graph view to use graph matching on, in order to apply transfomrations.
+ */
+void fuseBatchNorm(std::shared_ptr<GraphView> graphView);
 
 }
 
-
-#endif /* AIDGE_CORE_UTILS_RECIPIES_H_ */
\ No newline at end of file
+#endif /* AIDGE_CORE_UTILS_RECIPIES_H_ */
diff --git a/include/aidge/utils/Registrar.hpp b/include/aidge/utils/Registrar.hpp
index 98749c1349bad644dee2c1a8549559939791f71c..de543e95a16475c4443164af7be5c379d6554f8d 100644
--- a/include/aidge/utils/Registrar.hpp
+++ b/include/aidge/utils/Registrar.hpp
@@ -34,6 +34,7 @@ public:
     static std::map<Key, std::function<Func>>& registry()
     {
         #ifdef PYBIND
+        #define _CRT_SECURE_NO_WARNINGS
         if (std::getenv("AIDGE_CORE_WITH_PYBIND")){
             std::string name = std::string("registrar_")+typeid(Registrable<DerivedClass, Key, Func>).name();
             static auto shared_data = reinterpret_cast<std::map<Key, std::function<Func>> *>(py::get_shared_data(name));
diff --git a/include/aidge/utils/TensorUtils.hpp b/include/aidge/utils/TensorUtils.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..6387619546c66922e48cf95a8a56487d4b0d0641
--- /dev/null
+++ b/include/aidge/utils/TensorUtils.hpp
@@ -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
+ *
+ ********************************************************************************/
+
+#ifndef AIDGE_CORE_UTILS_TENSOR_UTILS_H_
+#define AIDGE_CORE_UTILS_TENSOR_UTILS_H_
+#include <cmath>  // std::abs
+#include "aidge/data/Tensor.hpp"
+
+/**
+ * @brief Compare two :cpp:class:`Aidge::Tensor` value wise. The comparison function is:
+ *
+ * |t1-t2| <= absolute + relative * |t2|
+ *
+ * If a tensor value is different from the other tensor return False
+ * If the tensor does not have the same size, return False
+ * If the datatype is not the same between each tensor return False
+ * If the templated type does not correspond to the datatype of each tensor, raise an assertion error
+ *
+ * @tparam T should correspond to the type of the tensor, define the type of the absolute and relative error
+ * @param t1  first :cpp:class:`Aidge::Tensor` to test
+ * @param t2  second :cpp:class:`Aidge::Tensor` to test
+ * @param relative relative difference allowed (should be betwen 0 and 1)
+ * @param absolute absolute error allowed (shoulmd be positive)
+ * @return true if both tensor are approximately equal and have the datatype, shape. Else return false
+ */
+template <typename T>
+bool approxEq(Aidge::Tensor t1, Aidge::Tensor t2, float relative, float absolute){
+    assert(t1.dataType() == t2.dataType());
+    assert(t1.dataType() == NativeType<T>::type);
+    assert(relative >= 0);
+    assert(absolute >= 0 && absolute<=1);
+
+    if (t1.size() != t2.size()){
+        return false;
+    }
+    for(size_t i; i < t1.size(); ++i){
+        if (static_cast<float>(std::abs(t1.get<T>(i) - t2.get<T>(i))) > (absolute + (relative * static_cast<float>(std::abs(t2.get<T>(i)))))){
+            return false;
+        }
+    }
+    return true;
+}
+
+#endif /* AIDGE_CORE_UTILS_TENSOR_UTILS_H_s */
diff --git a/python_binding/data/pybind_Tensor.cpp b/python_binding/data/pybind_Tensor.cpp
index d6442723ecc79527e8eaa7d3e03a466c085dfa58..31470e0eb2c50b5386b64498f89419801b133d3a 100644
--- a/python_binding/data/pybind_Tensor.cpp
+++ b/python_binding/data/pybind_Tensor.cpp
@@ -26,10 +26,10 @@ namespace Aidge {
 
 template<typename T>
 void addCtor(py::class_<Tensor,
-                        std::shared_ptr<Tensor>, 
-                        Data, 
+                        std::shared_ptr<Tensor>,
+                        Data,
                         Registrable<Tensor,
-                                    std::tuple<std::string, DataType>, 
+                                    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) {
         /* Request a buffer descriptor from Python */
@@ -46,24 +46,27 @@ void addCtor(py::class_<Tensor,
         }else{
             printf("Warning : Could not use aidge_cpu backend, verify you have `import aidge_cpu`\n");
         }
-        
+
         return newTensor;
-    }));
+    }))
+    .def("__setitem__", (void (Tensor::*)(std::size_t, T)) &Tensor::set)
+    .def("__setitem__", (void (Tensor::*)(std::vector<std::size_t>, T)) &Tensor::set)
+    ;
 }
 
 
 void init_Tensor(py::module& m){
     py::class_<Registrable<Tensor,
-                           std::tuple<std::string, DataType>, 
+                           std::tuple<std::string, DataType>,
                            std::unique_ptr<TensorImpl>(const Tensor&)>,
                std::shared_ptr<Registrable<Tensor,
-                                           std::tuple<std::string, DataType>, 
+                                           std::tuple<std::string, DataType>,
                                            std::unique_ptr<TensorImpl>(const Tensor&)>>>(m,"TensorRegistrable");
 
-    py::class_<Tensor, std::shared_ptr<Tensor>, 
-               Data, 
+    py::class_<Tensor, std::shared_ptr<Tensor>,
+               Data,
                Registrable<Tensor,
-                           std::tuple<std::string, DataType>, 
+                           std::tuple<std::string, DataType>,
                            std::unique_ptr<TensorImpl>(const Tensor&)>> pyClassTensor
         (m,"Tensor", py::multiple_inheritance(), py::buffer_protocol());
 
@@ -74,6 +77,8 @@ void init_Tensor(py::module& m){
     .def("size", &Tensor::size)
     .def("resize", (void (Tensor::*)(const std::vector<DimSize_t>&)) &Tensor::resize)
     .def("has_impl", &Tensor::hasImpl)
+    .def("get_coord", &Tensor::getCoord)
+    .def("get_idx", &Tensor::getIdx)
     .def_static("get_available_backends", &Tensor::getAvailableBackends)
     .def("__str__", [](Tensor& b) {
         return b.toString();
@@ -82,15 +87,27 @@ void init_Tensor(py::module& m){
         return b.size();
     })
     .def("__getitem__", [](Tensor& b, size_t idx)-> py::object {
-        // TODO : Should return error if backend not compatible with get
         if (idx >= b.size()) throw py::index_error();
         switch(b.dataType()){
             case DataType::Float64:
-                return py::cast(static_cast<double*>(b.getImpl()->rawPtr())[idx]);
+                return py::cast(b.get<double>(idx));
+            case DataType::Float32:
+                return py::cast(b.get<float>(idx));
+            case DataType::Int32:
+                return py::cast(b.get<int>(idx));
+            default:
+                return py::none();
+        }
+    })
+    .def("__getitem__", [](Tensor& b, std::vector<size_t> coordIdx)-> py::object {
+        if (b.getIdx(coordIdx) >= b.size()) throw py::index_error();
+        switch(b.dataType()){
+            case DataType::Float64:
+                return py::cast(b.get<double>(coordIdx));
             case DataType::Float32:
-                return py::cast(static_cast<float*>(b.getImpl()->rawPtr())[idx]);
+                return py::cast(b.get<float>(coordIdx));
             case DataType::Int32:
-                return py::cast(static_cast<int*>(b.getImpl()->rawPtr())[idx]);
+                return py::cast(b.get<int>(coordIdx));
             default:
                 return py::none();
         }
@@ -126,12 +143,12 @@ void init_Tensor(py::module& m){
         }
 
         return py::buffer_info(
-            tensorImpl->rawPtr(),                       /* Pointer to buffer */
-            tensorImpl->scalarSize(),                   /* Size of one scalar */
-            dataFormatDescriptor,                /* Python struct-style format descriptor */
-            b.nbDims(),                                 /* Number of dimensions */
-            dims,                                       /* Buffer dimensions */
-            strides                                     /* Strides (in bytes) for each index */
+            tensorImpl->rawPtr(),       /* Pointer to buffer */
+            tensorImpl->scalarSize(),   /* Size of one scalar */
+            dataFormatDescriptor,       /* Python struct-style format descriptor */
+            b.nbDims(),                 /* Number of dimensions */
+            dims,                       /* Buffer dimensions */
+            strides                     /* Strides (in bytes) for each index */
         );
     });
 
@@ -142,6 +159,6 @@ void init_Tensor(py::module& m){
 // #if SIZE_MAX != 0xFFFFFFFF
     addCtor<double>(pyClassTensor);
 // #endif
-    
+
 }
 }
diff --git a/python_binding/operator/pybind_Matmul.cpp b/python_binding/operator/pybind_Matmul.cpp
index b6ae27289fabe1fe4dbeea60704a61373bc850cf..b0b3c3df6dfbd2c50969da40c2621dbbdf04178b 100644
--- a/python_binding/operator/pybind_Matmul.cpp
+++ b/python_binding/operator/pybind_Matmul.cpp
@@ -11,7 +11,7 @@
 
 #include <pybind11/pybind11.h>
 
-#include "aidge/operator/Matmul.hpp"
+#include "aidge/operator/MatMul.hpp"
 #include "aidge/utils/Parameter.hpp"
 #include "aidge/backend/OperatorImpl.hpp"
 #include "aidge/operator/Operator.hpp"
@@ -20,13 +20,13 @@
 namespace py = pybind11;
 namespace Aidge {
 
-void declare_Matmul(py::module &m) {
-  py::class_<Matmul_Op, std::shared_ptr<Matmul_Op>, Operator, PyAbstractParametrizable>(m, "Matmul_Op", py::multiple_inheritance());
+void declare_MatMul(py::module &m) {
+  py::class_<MatMul_Op, std::shared_ptr<MatMul_Op>, Operator, PyAbstractParametrizable>(m, "MatMul_Op", py::multiple_inheritance());
 
-  m.def("Matmul", &Matmul, py::arg("out_channels"), py::arg("name") = "");
+  m.def("MatMul", &MatMul, py::arg("out_channels"), py::arg("name") = "");
 }
 
-void init_Matmul(py::module &m) {
-  declare_Matmul(m);
+void init_MatMul(py::module &m) {
+  declare_MatMul(m);
 }
 } // namespace Aidge
diff --git a/python_binding/operator/pybind_Producer.cpp b/python_binding/operator/pybind_Producer.cpp
index ea9880800059e8993996e67138f89419c165fc4f..4714e096fba90aff5c4289c1f87486e411c21b78 100644
--- a/python_binding/operator/pybind_Producer.cpp
+++ b/python_binding/operator/pybind_Producer.cpp
@@ -26,18 +26,19 @@ template <DimIdx_t DIM>
 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&)>(&Producer), py::arg("dims"), py::arg("name") = "");
-    
+
 }
 
 
 void init_Producer(py::module &m) {
     py::class_<Producer_Op,  std::shared_ptr<Producer_Op>, Operator>(
-        m, 
-        "ProducerOp", 
+        m,
+        "ProducerOp",
         py::multiple_inheritance())
-    .def("dims", &Producer_Op::dims);
+    .def("dims", &Producer_Op::dims)
+    .def("set_output_tensor", &Producer_Op::setOutputTensor);
     m.def("Producer", static_cast<std::shared_ptr<Node>(*)(const std::shared_ptr<Tensor>, const std::string&)>(&Producer), py::arg("tensor"), py::arg("name") = "");
-    
+
     declare_Producer<1>(m);
     declare_Producer<2>(m);
     declare_Producer<3>(m);
diff --git a/python_binding/pybind_core.cpp b/python_binding/pybind_core.cpp
index 78418d51a5c410cb56bb8421fd7f3dc6ec6d32db..db116d132ec8ffc504b2c0910eafc1a3da34534f 100644
--- a/python_binding/pybind_core.cpp
+++ b/python_binding/pybind_core.cpp
@@ -28,7 +28,7 @@ void init_ConvDepthWise(py::module&);
 void init_FC(py::module&);
 void init_GenericOperator(py::module&);
 void init_LeakyReLU(py::module&);
-void init_Matmul(py::module&);
+void init_MatMul(py::module&);
 void init_MaxPooling(py::module&);
 void init_Producer(py::module&);
 void init_ReLU(py::module&);
@@ -46,7 +46,7 @@ void init_GRegex(py::module&);
 void init_Recipies(py::module&);
 
 void init_Scheduler(py::module&);
-
+void init_TensorUtils(py::module&);
 
 void set_python_flag(){
     // Set an env variable to know if we run with ypthon or cpp
@@ -75,7 +75,7 @@ void init_Aidge(py::module& m){
     init_FC(m);
     init_GenericOperator(m);
     init_LeakyReLU(m);
-    init_Matmul(m);
+    init_MatMul(m);
     init_MaxPooling(m);
     init_ReLU(m);
     init_Softmax(m);
@@ -86,6 +86,7 @@ void init_Aidge(py::module& m){
     init_GRegex(m);
     init_Recipies(m);
     init_Scheduler(m);
+    init_TensorUtils(m);
 }
 
 PYBIND11_MODULE(aidge_core, m) {
diff --git a/python_binding/recipies/pybind_Recipies.cpp b/python_binding/recipies/pybind_Recipies.cpp
index b4147dcb4fb82dbfe9f5b4605604725c6945ece9..93c131ef7417135bfdbc657c5c809339430616ed 100644
--- a/python_binding/recipies/pybind_Recipies.cpp
+++ b/python_binding/recipies/pybind_Recipies.cpp
@@ -20,24 +20,51 @@ namespace py = pybind11;
 
 namespace Aidge {
 void init_Recipies(py::module &m) {
-  m.def("fuse_mul_add", &fuseMulAdd, py::arg("nodes"), R"mydelimiter(
-    Recipie to Fuse MatMul and Add operators into an `aidge.FC` operator.
-    
-    Parameters
-    ----------
+
+
+  m.def("fuse_mul_add", static_cast<void(*)(std::shared_ptr<GraphView>)>(fuseMulAdd), py::arg("graph_view"), R"mydelimiter(
+    Recipie to Fuse MatMul and Add operators into an :py:class:`aidge_core.FC` operator.
+
+    :param graph_view: Graph view on which we want to apply the recipie
+    :type graph_view: :py:class:`aidge_core.GraphView`
+    )mydelimiter");
+  m.def("fuse_mul_add", static_cast<void(*)(std::set<std::shared_ptr<Node>>)>(fuseMulAdd), py::arg("nodes"), R"mydelimiter(
+    Recipie to Fuse MatMul and Add operators into an :py:class:`aidge_core.FC` operator.
+
     :param nodes: The MatMul and Add nodes to fuse.
-    :type nodes: list of `aidge.node`
+    :type nodes: list of :py:class:`aidge_core.Node`
+    )mydelimiter");
+
+  m.def("remove_flatten", static_cast<void(*)(std::shared_ptr<GraphView>)>(removeFlatten), py::arg("graph_view"), R"mydelimiter(
+    Recipie to remove a flatten operator.
 
+    :param graph_view: Graph view on which we want to apply the recipie
+    :type graph_view: :py:class:`aidge_core.GraphView`
     )mydelimiter");
-  m.def("remove_flatten", &removeFlatten, py::arg("nodes"), R"mydelimiter(
+  m.def("remove_flatten", static_cast<void(*)(std::set<std::shared_ptr<Node>>)>(removeFlatten), py::arg("nodes"), R"mydelimiter(
     Recipie to remove a flatten operator.
-    
-    Parameters
-    ----------
+
     :param nodes: The flatten operator to remove.
-    :type nodes: list of `aidge.node`
+    :type nodes: list of :py:class:`aidge_core.Node`
+    )mydelimiter");
+  m.def("fuse_mul_add", static_cast<void(*)(std::set<std::shared_ptr<Node>>)>(fuseMulAdd), py::arg("nodes"), R"mydelimiter(
+    Recipie to Fuse MatMul and Add operators into an :py:class:`aidge_core.FC` operator.
 
+    :param nodes: The MatMul and Add nodes to fuse.
+    :type nodes: list of :py:class:`aidge_core.Node`
+    )mydelimiter");
+
+  m.def("fuse_batchnorm", static_cast<void(*)(std::shared_ptr<GraphView>)>(fuseBatchNorm), py::arg("graph_view"), R"mydelimiter(
+    Recipie to remove a flatten operator.
+
+    :param graph_view: Graph view on which we want to apply the recipie
+    :type graph_view: :py:class:`aidge_core.GraphView`
+    )mydelimiter");
+  m.def("fuse_batchnorm", static_cast<void(*)(std::set<std::shared_ptr<Node>>)>(fuseBatchNorm), py::arg("nodes"), R"mydelimiter(
+    Recipie to remove a flatten operator.
+
+    :param nodes: The flatten operator to remove.
+    :type nodes: list of :py:class:`aidge_core.Node`
     )mydelimiter");
-  
 }
 } // namespace Aidge
diff --git a/python_binding/utils/pybind_TensorUtils.cpp b/python_binding/utils/pybind_TensorUtils.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..78825a5f3b8d45f22f76c57bd780dc7019fbc123
--- /dev/null
+++ b/python_binding/utils/pybind_TensorUtils.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 <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+
+#include <string>
+
+#include "aidge/utils/TensorUtils.hpp"
+
+namespace py = pybind11;
+
+namespace Aidge {
+
+template<typename T>
+void addTensorUtilsFunction(py::module &m){
+    m.def("approx_eq",
+    & approxEq<T>,
+    py::arg("t1"),
+    py::arg("t2"),
+    py::arg("relative"),
+    py::arg("absolute"),
+    R"mydelimiter(
+        Compare two :cpp:class:`Aidge::Tensor` value wise. The comparison function is:
+            |t1-t2| <= absolute + relative * |t2|
+
+        If a tensor value is different from the other tensor return False
+        If the tensor does not have the same size, return False
+        If the datatype is not the same between each tensor return False
+        If the templated type does not correspond to the datatype of each tensor, raise an assertion error
+
+        :param t1: first tensor to test
+        :type t1: :py:class:`aidge_core.Tensor`
+        :param t2: second tensor to test
+        :type t2: :py:class:`aidge_core.Tensor`
+        :param relative: relative difference allowed (should be betwen 0 and 1)
+        :type relative: float
+        :param absolute: absolute error allowed (shoulmd be positive)
+        :type absolute: float
+        )mydelimiter");
+}
+
+void init_TensorUtils(py::module &m) {
+    addTensorUtilsFunction<float>(m);
+    addTensorUtilsFunction<double>(m);
+    addTensorUtilsFunction<int>(m);
+    addTensorUtilsFunction<long>(m);
+}
+} // namespace Aidge
diff --git a/src/graph/GraphView.cpp b/src/graph/GraphView.cpp
index bbf895285e0e00d1132eb1f46c7e67a455d705d7..03b2a9adb439eb00d0ba59a13fead4f25d617b36 100644
--- a/src/graph/GraphView.cpp
+++ b/src/graph/GraphView.cpp
@@ -519,17 +519,17 @@ void Aidge::GraphView::link(std::string /*name1_inID*/,
   printf("Not implemented yet.\n");
 }
 
-void Aidge::GraphView::insertParent(NodePtr childNode, 
-                  NodePtr newParentNode, 
-                  IOIndex_t childInputTensorIdx, 
-                  IOIndex_t newParentInputTensorIdx, 
+void Aidge::GraphView::insertParent(NodePtr childNode,
+                  NodePtr newParentNode,
+                  IOIndex_t childInputTensorIdx,
+                  IOIndex_t newParentInputTensorIdx,
                   IOIndex_t newParentOutputTensorIdx){
   NodePtr currentParentNode = childNode->getParent(childInputTensorIdx);
   const IOIndex_t currentParentOutputTensorIdx = childNode->input(childInputTensorIdx).second;
-  // Remove child from current parent & current Parent from child 
+  // Remove child from current parent & current Parent from child
   currentParentNode->removeChild(childNode, currentParentOutputTensorIdx);
 
-  // Add child 
+  // Add child
   currentParentNode->addChild(newParentNode,currentParentOutputTensorIdx, newParentInputTensorIdx);
   newParentNode->addChild(childNode, newParentOutputTensorIdx, childInputTensorIdx);
 
@@ -542,9 +542,8 @@ bool Aidge::GraphView::replaceWith(std::set<std::shared_ptr<Node>> newNodes) {
   assert(mNodes.size()>0 && "There must be at least one Node to replace");
 
   bool replacable;
-  std::shared_ptr<Node> previousInputNode;
-  std::shared_ptr<Node> newInputNode;
-  std::shared_ptr<Node> previousOutputNode;
+  std::shared_ptr<Node> previousInputNode = (*inputNodes().begin());
+  std::shared_ptr<Node> previousOutputNode = (*outputNodes().begin());
   std::shared_ptr<Node> newOutputNode;
 
   auto gNew = std::make_shared<GraphView>();
@@ -552,18 +551,15 @@ bool Aidge::GraphView::replaceWith(std::set<std::shared_ptr<Node>> newNodes) {
 
   if (newNodes.empty()) {
     replacable = (outputNodes().size() == 1) &&
-                      (inputNodes().size() == 1) &&
-                      ((*outputNodes().begin())->nbOutputs() == 1) &&
-                      ((*inputNodes().begin())->nbInputs() == 1);
-    previousOutputNode = (*outputNodes().begin());
-    previousInputNode = (*inputNodes().begin());
+                 (inputNodes().size() == 1) &&
+                 ((*outputNodes().begin())->nbOutputs() == 1) &&
+                 ((*inputNodes().begin())->nbDataInputs() == 1);
     newOutputNode = previousInputNode->input(0).first;
   } else {
-    replacable = ((outputNodes().size() == gNew->outputNodes().size()) &&
-                     (outputNodes().size() == 1));
-    previousOutputNode = (*outputNodes().begin());
     newOutputNode = (*gNew->outputNodes().begin());
-    replacable = replacable && (previousOutputNode->nbOutputs() == newOutputNode->nbOutputs());
+    replacable = (outputNodes().size() == gNew->outputNodes().size()) &&
+                 (outputNodes().size() == 1) &&
+                 (previousOutputNode->nbOutputs() == newOutputNode->nbOutputs());
   }
 
   if (replacable) {
diff --git a/src/recipies/FuseBatchNorm.cpp b/src/recipies/FuseBatchNorm.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2250bf87c66ff12b702bb2f01c7429c19ed41606
--- /dev/null
+++ b/src/recipies/FuseBatchNorm.cpp
@@ -0,0 +1,146 @@
+/********************************************************************************
+ * 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 <set>
+#include <cassert>
+#include <memory>
+#include <string>
+#include "aidge/operator/FC.hpp"
+#include "aidge/operator/BatchNorm.hpp"
+#include "aidge/operator/Conv.hpp"
+
+#include "aidge/utils/Recipies.hpp"
+#include "aidge/graph/GraphView.hpp"
+#include "aidge/graph/Node.hpp"
+#include "aidge/operator/Producer.hpp"
+#include "aidge/operator/GenericOperator.hpp"
+// Graph Regex
+#include "aidge/graphmatching/GRegex.hpp"
+#include "aidge/graphmatching/NodeRegex.hpp"
+using namespace Aidge;
+
+void Aidge::fuseBatchNorm(std::set<std::shared_ptr<Node>> nodes){
+
+    assert(nodes.size() == 2 && "Wrong number of nodes to replace\n");
+
+    // Assert the nodes types are correct to be fused
+    std::shared_ptr<Node> conv;
+    std::shared_ptr<Node> batchnorm;
+    for (const auto& element : nodes) {
+        assert((element->type() == "Conv" || element->type() == "BatchNorm") && "Wrong type for the nodes to replace");
+        if (element->type() == "Conv"){
+            conv = element;
+        }
+        else if (element->type() == "BatchNorm") {
+            batchnorm = element;
+        }
+    }
+    // TODO : check if batchnorm is the only child of the Conv or FC
+    std::shared_ptr<Tensor> scale  = batchnorm->input(1).first->getOperator()->getOutput(batchnorm->input(1).second);
+    std::shared_ptr<Tensor> shift  = batchnorm->input(2).first->getOperator()->getOutput(batchnorm->input(2).second);
+    std::shared_ptr<Tensor> b_mean = batchnorm->input(3).first->getOperator()->getOutput(batchnorm->input(3).second);
+    std::shared_ptr<Tensor> b_var  = batchnorm->input(4).first->getOperator()->getOutput(batchnorm->input(4).second);
+
+
+    // TODO : Find a way to remove the template
+    const float epsilon = std::static_pointer_cast<BatchNorm_Op<2>>(batchnorm->getOperator())->get<float>("Epsilon");
+    DimSize_t convOutDims = std::static_pointer_cast<Conv_Op<2>>(conv->getOperator())->get<DimSize_t>("OutChannels");
+
+
+    assert(scale->size()  == convOutDims);
+    assert(shift->size()  == convOutDims);
+    assert(b_mean->size() == convOutDims);
+    assert(b_var->size()  == convOutDims);
+    assert(epsilon > 0.0);
+    // TODO : no no_bias attribute ?
+    float meanVariance = 0.0;
+    unsigned int count = 0;
+
+    for (std::size_t output = 0; output < convOutDims; ++output) {
+        // TODO : get suppose datatype is float ..
+        if (b_var->get<float>(output) > 1.0e-12) {
+            meanVariance += b_var->get<float>(output);
+            ++count;
+        }
+        else {
+            printf("Zero-variance: %s [%lu]\n", conv->name().c_str(), output);
+        }
+    }
+    if (count > 0)
+        meanVariance /= count;
+    else {
+        printf("variance < 1e-12 for all outputs! Is the network correctly trained?\n");
+    }
+
+    const DimSize_t channelsSize = std::static_pointer_cast<Conv_Op<2>>(conv->getOperator())->get<DimSize_t>("InChannels");
+
+    // TODO : suppose we have Conv2D ...
+    const std::array<DimSize_t, 2> kernelDims = std::static_pointer_cast<Conv_Op<2>>(conv->getOperator())->get<std::array<DimSize_t, 2>>("KernelDims");
+
+    std::shared_ptr<Tensor> weight  = conv->input(1).first->getOperator()->getOutput(conv->input(1).second);
+    std::shared_ptr<Tensor> bias  = conv->input(2).first->getOperator()->getOutput(conv->input(2).second);
+
+    for (std::size_t output = 0; output < convOutDims; ++output) {
+        // Corrected for zero-variance issue:
+        // "A Quantization-Friendly Separable Convolution for MobileNets"
+        // https://arxiv.org/pdf/1803.08607.pdf
+        // to help post-training quantization
+        const float factor = scale->get<float>(output)
+            / std::sqrt(epsilon + ((b_var->get<float>(output) > 1.0e-12 || count == 0)
+                        ? b_var->get<float>(output) : meanVariance));
+        // Weights adjustments
+        for (std::size_t channel = 0; channel < channelsSize; ++channel) {
+            // TODO : Suppose kerneldims = 2
+            for(std::size_t k0 = 0; k0 < kernelDims[0]; ++ k0){
+                for(std::size_t k1 = 0; k1 < kernelDims[1]; ++ k1){
+                    std::vector<DimSize_t> currentIdx = {output, channel, k0, k1};
+                    // TODO : suppose weights are float
+                    float weightValue = weight->get<float>(currentIdx);
+                    weight->set<float>(currentIdx, weightValue*factor); // Update check it update Conv weights
+                }
+            }
+        }
+
+        // TODO : check if noBias==true is set, then set biasValue to 0
+        float biasValue = bias->get<float>(output);
+
+        biasValue = shift->get<float>(output) + (biasValue - b_mean->get<float>(output)) * factor;
+
+        bias->set<float>(output, biasValue);
+
+    }
+    auto g = std::make_shared<GraphView>();
+    g->add(std::set<std::shared_ptr<Node>>({
+        batchnorm,
+        batchnorm->input(1).first,
+        batchnorm->input(2).first,
+        batchnorm->input(3).first,
+        batchnorm->input(4).first
+    }));
+    g->replaceWith({});
+
+}
+
+void Aidge::fuseBatchNorm(std::shared_ptr<GraphView> graphView){
+    std::map<std::string,NodeRegex*> nodesRegex ;
+    nodesRegex["BatchNorm"] = new NodeRegex("BatchNorm");
+    nodesRegex["Conv"] = new NodeRegex("Conv");
+    nodesRegex["FC"] = new NodeRegex("FC");
+
+
+    std::vector<std::string> seqRegex;
+    seqRegex.push_back("Conv -> BatchNorm;"); // TODO: Add (Conv | FC)
+    GRegex GReg(nodesRegex, seqRegex);
+    Match matches = GReg.match(graphView);
+    std::vector<std::set<std::shared_ptr<Node>>> matchNodes = matches.getMatchNodes();
+    for (size_t i = 0; i < matches.getNbMatch(); ++i) {
+        fuseBatchNorm(matchNodes[i]);
+    }
+}
diff --git a/src/recipies/FuseMulAdd.cpp b/src/recipies/FuseMulAdd.cpp
index 561d25776a28f1aad8f8c943711887ec6661a10c..1de79890f9b597c4baff7427e01d7217f9695a44 100644
--- a/src/recipies/FuseMulAdd.cpp
+++ b/src/recipies/FuseMulAdd.cpp
@@ -20,21 +20,18 @@
 #include "aidge/graph/Node.hpp"
 #include "aidge/operator/Producer.hpp"
 #include "aidge/operator/GenericOperator.hpp"
-
+// Graph Regex
+#include "aidge/graphmatching/GRegex.hpp"
+#include "aidge/graphmatching/NodeRegex.hpp"
 using namespace Aidge;
 
-/**
- * @brief Merge MatMul and Add Node into FC.
- * 
- * @param nodes Strict set of Node to merge.
- */
 void Aidge::fuseMulAdd(std::set<std::shared_ptr<Node>> nodes){
     // Fuse Mulmat & Add into FC
     // Inputs : old nodes (pointers on mul & add)
-    
+
     assert(nodes.size() == 2 && "Wrong number of nodes to replace\n");
     // Too bad we lose information on the type after matching, how to keep the information after matching (not only for the type) ?
-    
+
     // Step 0 : Assert the nodes types are correct to be fused
     std::shared_ptr<Node> add;
     std::shared_ptr<Node> matmul;
@@ -53,7 +50,7 @@ void Aidge::fuseMulAdd(std::set<std::shared_ptr<Node>> nodes){
     auto producer_add_bias = add->input(1);
     Tensor& bias_tensor = (producer_add_bias.first)->getOperator()->output(0);
 
-    // Instanciate FC  
+    // Instanciate FC
     //std::shared_ptr<Node> fc = FC(dim[0], false, "Fc");
     std::shared_ptr<Node> fc = std::make_shared<Node>(std::make_shared<FC_Op>(bias_tensor.dims()[0], false));
 
@@ -61,10 +58,12 @@ void Aidge::fuseMulAdd(std::set<std::shared_ptr<Node>> nodes){
     // link weights & bias
     if (matmul->getParent(1)==nullptr) {
         matmul->getParent(0)->addChild(fc, 0, 1);
+        printf("MatMul out[1] == nullptr !\n");
     } else {
+        printf("MatMul out[1] != nullptr !\n");
         if (matmul->getParent(0)!=nullptr)
             matmul->getParent(0)->addChild(fc, 0, 0);
-        matmul->getParent(1)->addChild(fc, 0, 1);
+        matmul->input(1).first->addChild(fc, 0, 1);
     }
     (producer_add_bias.first)->addChild(fc,0,2);
 
@@ -74,7 +73,22 @@ void Aidge::fuseMulAdd(std::set<std::shared_ptr<Node>> nodes){
         // 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 nodeToReplace = std::make_shared<GraphView>();
-    nodeToReplace->add(nodes);
+    nodeToReplace->add(nodes, false);
     nodeToReplace->replaceWith({fc});
 
-}
\ No newline at end of file
+}
+
+void Aidge::fuseMulAdd(std::shared_ptr<GraphView> graphView){
+
+    std::map<std::string,NodeRegex*> nodesRegex ;
+    nodesRegex["MatMul"] = new NodeRegex("MatMul");
+    nodesRegex["Add"] = new NodeRegex("Add");
+    std::vector<std::string> seqRegex;
+    seqRegex.push_back("MatMul -> Add;");
+    GRegex GReg(nodesRegex, seqRegex);
+    Match matches = GReg.match(graphView);
+    std::vector<std::set<std::shared_ptr<Node>>> matchNodes = matches.getMatchNodes();
+    for (size_t i = 0; i < matches.getNbMatch(); ++i) {
+        fuseMulAdd(matchNodes[i]);
+    }
+}
diff --git a/src/recipies/RemoveFlatten.cpp b/src/recipies/RemoveFlatten.cpp
index cc3c3324e40636a1edcbc73cdc4a9dcfeec8a026..9096c107ba505f5f18993a761273552408db721b 100644
--- a/src/recipies/RemoveFlatten.cpp
+++ b/src/recipies/RemoveFlatten.cpp
@@ -15,10 +15,38 @@
 #include "aidge/graph/GraphView.hpp"
 #include "aidge/utils/Recipies.hpp"
 
+// Graph Regex
+#include "aidge/graphmatching/GRegex.hpp"
+#include "aidge/graphmatching/NodeRegex.hpp"
+
+
 namespace Aidge {
     void removeFlatten(std::set<std::shared_ptr<Node>> nodes) {
+        assert(nodes.size() == 2 && "Wrong number of nodes to replace\n");
+        std::shared_ptr<Node> flatten;
+        for (const auto& element : nodes) {
+            assert((element->type() == "FC" || element->type() == "Flatten") && "Wrong type for the nodes to replace");
+            if (element->type() == "Flatten"){
+                flatten = element;
+            }
+        }
         auto g = std::make_shared<GraphView>();
-        g->add(std::set<std::shared_ptr<Node>>({nodes}));
+        // TODO : avoid using replace_with and use a remove method instead
+        g->add(std::set<std::shared_ptr<Node>>({flatten}));
         g->replaceWith({});
     }
-}
\ No newline at end of file
+
+    void removeFlatten(std::shared_ptr<GraphView> graphView){
+        std::map<std::string,NodeRegex*> nodesRegex ;
+        nodesRegex["Flatten"] = new NodeRegex("Flatten");
+        nodesRegex["FC"] = new NodeRegex("FC");
+        std::vector<std::string> seqRegex;
+        seqRegex.push_back("Flatten->FC;");
+        GRegex GReg(nodesRegex, seqRegex);
+        Match matches = GReg.match(graphView);
+        std::vector<std::set<std::shared_ptr<Node>>> matchNodes = matches.getMatchNodes();
+        for (size_t i = 0; i < matches.getNbMatch(); ++i) {
+            removeFlatten(matchNodes[i]);
+        }
+    }
+}
diff --git a/src/scheduler/Scheduler.cpp b/src/scheduler/Scheduler.cpp
index dc0768d2b6f7a1dd46fc0a8523b950011f7dcf5d..4dc8eb5c84ddb25546a32a672bdc84685a6f79f0 100644
--- a/src/scheduler/Scheduler.cpp
+++ b/src/scheduler/Scheduler.cpp
@@ -34,6 +34,11 @@ void drawProgressBar(double progress, int barWidth, const std::string& additiona
 }
 
 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
+
     // setup initial producers list
     mComputationNumber = 0;
     std::set<std::shared_ptr<Node>> producers;
@@ -74,16 +79,16 @@ void Aidge::SequentialScheduler::generateScheduling(bool verbose) {
                        "\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),
+                    printf("%zu/%zu\n\t\t\t", consumer->getOperator()->getNbConsumedData(inId),
                            consumer->getOperator()->getNbRequiredData(inId));
                 }
-                printf("%ld/%ld", consumer->getOperator()->getNbConsumedData(static_cast<IOIndex_t>(consumer->nbInputs()) - 1),
+                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("%ld\n\t\t\t", consumer->getOperator()->getNbProducedData(outId));
+                    printf("%zu\n\t\t\t", consumer->getOperator()->getNbProducedData(outId));
                 }
-                printf("%ld", consumer->getOperator()->getNbProducedData(static_cast<IOIndex_t>(consumer->nbOutputs()) - 1));
+                printf("%zu", consumer->getOperator()->getNbProducedData(static_cast<IOIndex_t>(consumer->nbOutputs()) - 1));
                 printf("\n");
             }
             bool isRunnable = true;
@@ -123,13 +128,13 @@ void Aidge::SequentialScheduler::generateScheduling(bool verbose) {
                     printf("%ld/%ld\n\t\t\t", consumer->getOperator()->getNbConsumedData(inId),
                            consumer->getOperator()->getNbRequiredData(inId));
                 }
-                printf("%ld/%ld", consumer->getOperator()->getNbConsumedData(static_cast<IOIndex_t>(consumer->nbInputs()) - 1),
+                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("%ld\n\t\t\t", consumer->getOperator()->getNbProducedData(outId));
+                    printf("%zu\n\t\t\t", consumer->getOperator()->getNbProducedData(outId));
                 }
-                printf("%ld", consumer->getOperator()->getNbProducedData(static_cast<IOIndex_t>(consumer->nbOutputs()) - 1));
+                printf("%zu", consumer->getOperator()->getNbProducedData(static_cast<IOIndex_t>(consumer->nbOutputs()) - 1));
                 printf("\n");
             }
             bool isStillConsumer = false;
@@ -180,35 +185,20 @@ void Aidge::SequentialScheduler::forward(bool forwardDims, bool verbose) {
     mScheduling.clear();
 
     this->generateScheduling();
-
-    // 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
+    int cpt = 0;
     for (const auto& runnable : mStaticSchedule) {
-        bool computationOverForConsumer = true;
-        for (IOIndex_t parentIDi = 0; parentIDi < runnable->nbInputs(); ++parentIDi) {
-            if (runnable->getOperator()->getNbConsumedData(parentIDi) <
-                runnable->getOperator()->getNbRequiredData(parentIDi)) {
-                computationOverForConsumer = false;
-                break;
-            }
-        }
-        if (computationOverForConsumer) {
-            computationOver.insert(runnable);
-        }
-
         if (verbose)
             printf("run: %s\n",
                     (runnable->type() + "_" + std::to_string(reinterpret_cast<uintptr_t>(runnable.get()))).c_str());
         else
-            drawProgressBar(static_cast<float>(computationOver.size()) / static_cast<float>(mComputationNumber), 50,
+            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");
diff --git a/unit_tests/recipies/Test_FuseMulAdd.cpp b/unit_tests/recipies/Test_FuseMulAdd.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..da53642055a3146c71a211ad7816f21c9b92d6cd
--- /dev/null
+++ b/unit_tests/recipies/Test_FuseMulAdd.cpp
@@ -0,0 +1,77 @@
+/********************************************************************************
+ * 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/backend/cpu/operator/AddImpl.hpp"
+// #include "aidge/backend/cpu/operator/ConvImpl.hpp"
+// #include "aidge/backend/cpu/operator/FCImpl.hpp"
+// #include "aidge/backend/cpu/operator/MatMulImpl.hpp"
+#include "aidge/data/Tensor.hpp"
+#include "aidge/graph/GraphView.hpp"
+#include "aidge/operator/Add.hpp"
+#include "aidge/operator/FC.hpp"
+#include "aidge/operator/MatMul.hpp"
+#include "aidge/operator/Producer.hpp"
+#include "aidge/utils/Recipies.hpp"
+
+namespace Aidge {
+
+TEST_CASE("[cpu/recipies] FuseMulAdd", "[FuseMulAdd][recipies]") {
+    // generate the original GraphView
+    auto matmul0 = MatMul(5, "matmul0");
+    auto add0 = Add<2>("add0");
+    auto matmul1 = MatMul(5, "matmul1");
+    auto add1 = Add<2>("add1");
+
+    auto b0 = Producer({5}, "B0");
+    auto w0 = Producer({5, 5}, "W0");
+    auto b1 = Producer({5}, "B1");
+    auto w1 = Producer({5,5},"W1");
+    auto input = Producer({2,5}, "input");
+
+    input->addChild(matmul0, 0, 0);
+    w0->addChild(matmul0, 0, 1);
+
+    matmul0->addChild(add0, 0, 0);
+    b0->addChild(add0, 0, 1);
+
+    add0->addChild(matmul1, 0, 0);
+    w1->addChild(matmul1, 0, 1);
+
+    matmul1->addChild(add1, 0, 0);
+    b1->addChild(add1, 0, 1);
+
+    auto g = std::make_shared<GraphView>();
+    g->add({matmul0, add0, matmul1, add1, b0, b1});
+
+    // Check original graph
+    REQUIRE(g->getNodes() ==
+            std::set<std::shared_ptr<Node>>({w0, matmul0, b0, add0, w1, matmul1, b1, add1}));
+    REQUIRE(((matmul0->getParent(0) == input) && (matmul0->getParent(1) == w0)));
+    REQUIRE(((add0->getParent(0) == matmul0) && (add0->getParent(1) == b0)));
+    REQUIRE(((matmul1->getParent(0) == add0) && (matmul1->getParent(1) == w1)));
+    REQUIRE(((add1->getParent(0) == matmul1) && (add1->getParent(1) == b1)));
+
+	// Transform GraphView inplace
+    fuseMulAdd(g);
+	g->save("bonjour");
+
+	// Check new GraphView
+	 std::set<std::shared_ptr<Node>> newNodes = g->getNodes();
+	REQUIRE(newNodes != std::set<std::shared_ptr<Node>>({w0, matmul0, b0, add0, w1, matmul1, b1, add1}));
+	REQUIRE(newNodes.size() == 6);
+	for (const auto& node : newNodes) {
+		REQUIRE(((node->type() == "Producer") || (node->type() == "FC")));
+	}
+}
+}  // namespace Aidge
\ No newline at end of file