diff --git a/CMakeLists.txt b/CMakeLists.txt
index a0d70035e0e150ec33dc4806bd02632debbf0a42..b497a3a49e73e71cea4158049ef8a284771d484c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -30,8 +30,15 @@ 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
+)
 
+FetchContent_MakeAvailable(fmt)
 
 ##############################################
 # Create target and set properties
@@ -64,6 +71,7 @@ if (PYBIND)
         )
 endif()
 
+target_link_libraries(${module_name} PUBLIC 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..24a44f513c75038b595be6ded0602ab858deed8b 100644
--- a/aidge_core-config.cmake.in
+++ b/aidge_core-config.cmake.in
@@ -1,4 +1,13 @@
 @PACKAGE_INIT@
+Include(FetchContent)
+
+FetchContent_Declare(
+    fmt
+    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
+    GIT_TAG        10.2.1 # or a later release
+)
+
+FetchContent_MakeAvailable(fmt)
 
 include(${CMAKE_CURRENT_LIST_DIR}/aidge_core-config-version.cmake)
 
diff --git a/include/aidge/backend/TensorImpl.hpp b/include/aidge/backend/TensorImpl.hpp
index 62f13acb3db81954a4fbb753a3e68e1c5a516402..782294a66b6c162e8a18345f2de7b9e6a560a560 100644
--- a/include/aidge/backend/TensorImpl.hpp
+++ b/include/aidge/backend/TensorImpl.hpp
@@ -141,7 +141,7 @@ 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);
     };
 
     /**
diff --git a/include/aidge/graph/GraphView.hpp b/include/aidge/graph/GraphView.hpp
index 813301a144682ba3e99de31ae324ffaedcc5209f..77a759d9b76fc915a703bdae69bcd73e827e4508 100644
--- a/include/aidge/graph/GraphView.hpp
+++ b/include/aidge/graph/GraphView.hpp
@@ -262,6 +262,25 @@ public:
      */
     NodePtr getNode(const std::string& nodeName) const;
 
+    /**
+     * Get the ranked list of nodes in the GraphView.
+     * 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
diff --git a/include/aidge/operator/AvgPooling.hpp b/include/aidge/operator/AvgPooling.hpp
index a2098ff36b40b78eb12a36fe28793e8dd73d9d9c..d7b77396f8d97afa3b73253390c1b095503d205b 100644
--- a/include/aidge/operator/AvgPooling.hpp
+++ b/include/aidge/operator/AvgPooling.hpp
@@ -111,7 +111,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]);
                 }
             }
 
diff --git a/include/aidge/operator/Conv.hpp b/include/aidge/operator/Conv.hpp
index be5fb3e393ced7ee7a53e27426b4247e48b478e8..4483c2df2c0e0d544684822982957cbe4edd91a9 100644
--- a/include/aidge/operator/Conv.hpp
+++ b/include/aidge/operator/Conv.hpp
@@ -133,7 +133,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]);
                 }
             }
 
diff --git a/include/aidge/operator/ConvDepthWise.hpp b/include/aidge/operator/ConvDepthWise.hpp
index 9d0c0bf408a2f634f96881cd339c330340d5e344..1a4c867b3dfd04d97a33bddfdc51c851a17da907 100644
--- a/include/aidge/operator/ConvDepthWise.hpp
+++ b/include/aidge/operator/ConvDepthWise.hpp
@@ -128,7 +128,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]);
                 }
             }
 
diff --git a/include/aidge/operator/Identity.hpp b/include/aidge/operator/Identity.hpp
index 57cd20311a4e4c98966af0af98b9fe4533155ea6..0fbb48339ab8eb085d1c16edc0fdf4116d9d69ce 100644
--- a/include/aidge/operator/Identity.hpp
+++ b/include/aidge/operator/Identity.hpp
@@ -79,27 +79,27 @@ public:
 
     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());
+            AIDGE_THROW_OR_ABORT(std::runtime_error, "{} 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_THROW_OR_ABORT(std::runtime_error, "{} Operator has {} outputs", type().c_str(), 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());
+            AIDGE_THROW_OR_ABORT(std::runtime_error, "{} 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_THROW_OR_ABORT(std::runtime_error, "{} Operator has {} outputs", type().c_str(), 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_THROW_OR_ABORT(std::runtime_error, "{} Operator has {} outputs", type().c_str(), nbInputs());
         }
         if (mInputs[outputIdx] == nullptr){
             return mOutputs[outputIdx]; // Input is not initialized with empty tensor
diff --git a/include/aidge/scheduler/Scheduler.hpp b/include/aidge/scheduler/Scheduler.hpp
index 81dc6d7cc461dfdbf286f0469ce2c58f2a767ad8..9904683ba2c70987b45260caa8503ee1edcfbbac 100644
--- a/include/aidge/scheduler/Scheduler.hpp
+++ b/include/aidge/scheduler/Scheduler.hpp
@@ -89,12 +89,6 @@ private:
     std::set<std::shared_ptr<Node>> getConsumers(const std::set<std::shared_ptr<Node>>& producers) const;
     PriorProducersConsumers getPriorProducersConsumers(const std::shared_ptr<Node>& node) const;
 
-    /**
-     * Return a std::map with corresponding node's name.
-     * TODO: Mutualise with similar code in GraphView::save()?
-    */
-    std::map<std::shared_ptr<Node>, std::string> getNodesName(bool verbose) const;
-
     /** @brief Shared ptr to the scheduled graph view */
     std::shared_ptr<GraphView> mGraphView;
     /** @brief List of SchedulingElement (i.e: Nodes with their computation time) */
diff --git a/include/aidge/utils/ErrorHandling.hpp b/include/aidge/utils/ErrorHandling.hpp
index f4a929867ec28e3e2fc9d0bdfedb6e6265c07adc..0abdb6bdeed88e7e4c67d80ba233e1c5f21e66f4 100644
--- a/include/aidge/utils/ErrorHandling.hpp
+++ b/include/aidge/utils/ErrorHandling.hpp
@@ -13,20 +13,17 @@
 #ifndef AIDGE_ERRORHANDLING_H_
 #define AIDGE_ERRORHANDLING_H_
 
-#include <cstdio>
 #include <memory>
 
-#define AIDGE_STRINGIZE_DETAIL(x) #x
-#define AIDGE_STRINGIZE(x) AIDGE_STRINGIZE_DETAIL(x)
+#include <fmt/format.h>
 
 #ifdef NO_EXCEPTION
 #define AIDGE_THROW_OR_ABORT(ex, ...) \
-do { std::printf(__VA_ARGS__); std::abort(); } while (false)
+do { fmt::print(__VA_ARGS__); std::abort(); } while (false)
 #else
 #include <stdexcept>
-#include "aidge/utils/Formatting.hpp"
 #define AIDGE_THROW_OR_ABORT(ex, ...) \
-throw ex(stringFormat(__VA_ARGS__))
+throw ex(fmt::format(__VA_ARGS__))
 #endif
 
 /**
@@ -35,7 +32,7 @@ throw ex(stringFormat(__VA_ARGS__))
  * 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)) { fmt::print("Assertion failed: " #stm " in {}:{}", __FILE__, __LINE__); \
     AIDGE_THROW_OR_ABORT(std::runtime_error, __VA_ARGS__); }
 
 /**
@@ -44,6 +41,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/Formatting.hpp b/include/aidge/utils/Formatting.hpp
deleted file mode 100644
index 54ed55f598bb6dcbb5bc01178a87b29f1d4d862e..0000000000000000000000000000000000000000
--- a/include/aidge/utils/Formatting.hpp
+++ /dev/null
@@ -1,74 +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_FORMATTING_H_
-#define AIDGE_FORMATTING_H_
-
-#include <memory>
-#include <string>
-#include <vector>
-
-namespace Aidge {
-// The code snippet below is licensed under CC0 1.0.
-template<typename ... Args>
-std::string stringFormat(const std::string& format, Args... args) {
-#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__)
-// Disable security warning on GCC
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Wformat-security"
-#elif defined(_MSC_VER)
-// Disable security warning on MSVC
-#pragma warning(push)
-#pragma warning(disable : 4774)
-#endif
-    int size_s = std::snprintf(nullptr, 0, format.c_str(), args...) + 1; // Extra space for '\0'
-    if (size_s <= 0) {
-        std::printf("Error during formatting.");
-        std::abort();
-    }
-    auto size = static_cast<size_t>(size_s);
-    std::unique_ptr<char[]> buf(new char[size]);
-    std::snprintf(buf.get(), size, format.c_str(), args...);
-    return std::string(buf.get(), buf.get() + size - 1); // We don't want the '\0' inside
-#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__)
-#pragma GCC diagnostic pop
-#elif defined(_MSC_VER)
-#pragma warning(pop) 
-#endif
-}
-
-/**
- * Print any iterable object in a std::string.
-*/
-template <class T, typename F>
-std::string print(const T& vec, const std::string& format, const F& func) {
-    std::string str = "{";
-    bool first = true;
-    for (const auto& val : vec) {
-        if (!first) {
-            str += ", ";
-        }
-        else {
-            first = false;
-        }
-        str += stringFormat(format, func(val));
-    }
-    str += "}";
-    return str;
-}
-
-template <class T>
-std::string print(const T& vec, const std::string& format) {
-    return print(vec, format, [](auto val){ return val; });
-}
-}
-
-#endif //AIDGE_FORMATTING_H_
diff --git a/include/aidge/utils/StaticAttributes.hpp b/include/aidge/utils/StaticAttributes.hpp
index a90a08b01915c461bc8951c08ee2dbd979b957de..e0c0d32c80ea970cf10cabc650537eef1b73b9b0 100644
--- a/include/aidge/utils/StaticAttributes.hpp
+++ b/include/aidge/utils/StaticAttributes.hpp
@@ -95,7 +95,7 @@ public:
             }
         }
 
-        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>
@@ -106,7 +106,7 @@ public:
             }
         }
 
-        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.c_str());
     }
 
     std::set<std::string> getAttrsName() const override final {
@@ -211,7 +211,7 @@ 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.c_str());
     };
     #endif
 
diff --git a/src/graph/GraphView.cpp b/src/graph/GraphView.cpp
index 7576d708ae635275747535772e09f8eb15748311..b4f8f8df41dfb93391ea6f8792a4c857df85ae2f 100644
--- a/src/graph/GraphView.cpp
+++ b/src/graph/GraphView.cpp
@@ -15,8 +15,10 @@
 #include <utility>
 #include <numeric>
 
+#include <fmt/format.h>
+#include <fmt/ranges.h>
+
 #include "aidge/utils/Types.h"
-#include "aidge/utils/Formatting.hpp"
 #include "aidge/graph/GraphView.hpp"
 #include "aidge/data/Tensor.hpp"
 #include "aidge/operator/OperatorTensor.hpp"
@@ -56,37 +58,28 @@ std::string Aidge::GraphView::name() const { return mName; }
 
 void Aidge::GraphView::setName(const std::string &name) { mName = name; }
 
-
 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");
 
-    std::map<const std::string, std::size_t> typeCounter;
-    std::map<std::shared_ptr<Node>, std::string> namePtrTable;
-
     // 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];
+    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[node_ptr] + "</em>"
+                : "\"" + node_ptr->name() + "\\n<sub><em>(" + node_ptr->type() + "#" + namePtrTable[node_ptr] + ")</em></sub>\"";
 
         if (node_ptr == mRootNode) {
-          std::fprintf(fp, "%s(%s):::rootCls\n", namePtrTable[node_ptr].c_str(),
+          std::fprintf(fp, "%s_%s(%s):::rootCls\n", node_ptr->type().c_str(), namePtrTable[node_ptr].c_str(),
                       givenName.c_str());
         }
         else {
-            if ((currentType != "Producer") || showProducers) {
-                std::fprintf(fp, "%s(%s)\n", namePtrTable[node_ptr].c_str(),
+            if ((node_ptr->type() != "Producer") || showProducers) {
+                std::fprintf(fp, "%s_%s(%s)\n", node_ptr->type().c_str(), namePtrTable[node_ptr].c_str(),
                             givenName.c_str());
             }
         }
@@ -108,15 +101,15 @@ void Aidge::GraphView::save(std::string path, bool verbose, bool showProducers)
                 std::string dims = "";
                 const auto op = std::dynamic_pointer_cast<OperatorTensor>(node_ptr->getOperator());
                 if (op && !op->getOutput(outputIdx)->dims().empty()) {
-                  dims += " " + print(op->getOutput(outputIdx)->dims(), "%u");
+                  dims += " " + fmt::format("{}", op->getOutput(outputIdx)->dims());
                 }
 
                 if (mNodes.find(child) != mNodes.end()) {
-                  std::fprintf(fp, "%s-->|\"%u%s&rarr;%u\"|%s\n", namePtrTable[node_ptr].c_str(),
-                              outputIdx, dims.c_str(), inputIdx, namePtrTable[child].c_str());
+                  std::fprintf(fp, "%s_%s-->|\"%u%s&rarr;%u\"|%s_%s\n", node_ptr->type().c_str(), namePtrTable[node_ptr].c_str(),
+                              outputIdx, dims.c_str(), inputIdx, child->type().c_str(), namePtrTable[child].c_str());
                 }
                 else if (verbose) {
-                  std::fprintf(fp, "%s-->|\"%u%s&rarr;%u\"|%p:::externalCls\n", namePtrTable[node_ptr].c_str(),
+                  std::fprintf(fp, "%s_%s-->|\"%u%s&rarr;%u\"|%p:::externalCls\n", node_ptr->type().c_str(), namePtrTable[node_ptr].c_str(),
                               outputIdx, dims.c_str(), inputIdx, static_cast<void*>(child.get()));
                 }
                 break;
@@ -131,8 +124,8 @@ 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());
+      std::fprintf(fp, "input%lu((in#%lu)):::inputCls--->|&rarr;%u|%s_%s\n", inputIdx, inputIdx,
+                  input.second, input.first->type().c_str(), namePtrTable[input.first].c_str());
       ++inputIdx;
     }
 
@@ -142,11 +135,11 @@ void Aidge::GraphView::save(std::string path, bool verbose, bool showProducers)
       std::string dims = "";
       const auto op = std::dynamic_pointer_cast<OperatorTensor>(output.first->getOperator());
       if (op && !op->getOutput(output.second)->dims().empty()) {
-        dims += " " + print(op->getOutput(output.second)->dims(), "%u");
+        dims += " " + fmt::format("{}", op->getOutput(output.second)->dims());
       }
 
-      std::fprintf(fp, "%s--->|\"%u%s&rarr;\"|output%lu((out#%lu)):::outputCls\n",
-                   namePtrTable[output.first].c_str(), output.second,
+      std::fprintf(fp, "%s_%s--->|\"%u%s&rarr;\"|output%lu((out#%lu)):::outputCls\n",
+                   output.first->type().c_str(), namePtrTable[output.first].c_str(), output.second,
                    dims.c_str(), outputIdx, outputIdx);
       ++outputIdx;
     }
@@ -155,13 +148,6 @@ void Aidge::GraphView::save(std::string path, bool verbose, bool showProducers)
     std::fprintf(fp, "classDef outputCls fill:#ffa\n");
     std::fprintf(fp, "classDef externalCls fill:#ccc\n");
     std::fprintf(fp, "classDef rootCls stroke:#f00\n");
-
-    if (verbose) {
-      for (const auto &c : typeCounter) {
-        std::printf("%s - %zu\n", c.first.c_str(), c.second);
-      }
-    }
-
     std::fprintf(fp, "\n");
     std::fclose(fp);
 }
@@ -436,6 +422,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 (nodesToRank.find(child) != nodesToRank.end()) {
+          rankedNodes.push_back(child);
+          nodesToRank.erase(child);
+        }
+      }
+    }
+
+    for (auto parent : curNode->getParents()) {
+      if (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;
diff --git a/src/operator/OperatorTensor.cpp b/src/operator/OperatorTensor.cpp
index 72a71814b1463395443c6a4504f2eef660ec1185..f8eba813a327b37a85e3c5868e356535fa6e92a6 100644
--- a/src/operator/OperatorTensor.cpp
+++ b/src/operator/OperatorTensor.cpp
@@ -21,7 +21,7 @@
 
 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_THROW_OR_ABORT(std::runtime_error, "{} Operator has {} inputs", type().c_str(), nbInputs());
     }
     if (strcmp((data)->type(), Tensor::Type) != 0) {
         AIDGE_THROW_OR_ABORT(std::runtime_error, "Input data must be of Tensor type");
@@ -31,7 +31,7 @@ void Aidge::OperatorTensor::associateInput(const Aidge::IOIndex_t inputIdx, cons
 
 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_THROW_OR_ABORT(std::runtime_error, "{} Operator only accepts Tensors as inputs", type().c_str());
     }
     if (getInput(inputIdx)) {
         *mInputs[inputIdx] = *std::dynamic_pointer_cast<Tensor>(data);
@@ -44,7 +44,7 @@ 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_THROW_OR_ABORT(std::runtime_error, "{} Operator only accepts Tensors as inputs", type().c_str());
     }
     if (getInput(inputIdx)) {
         *mInputs[inputIdx] = std::move(*std::dynamic_pointer_cast<Tensor>(data));
@@ -55,34 +55,34 @@ void Aidge::OperatorTensor::setInput(const Aidge::IOIndex_t inputIdx, std::share
 
 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_THROW_OR_ABORT(std::runtime_error, "{} Operator has {} inputs", type().c_str(), 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());
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "{} 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());
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "{} Operator has {} outputs", type().c_str(), nbOutputs());
     }
     *mOutputs[outputIdx] = *std::dynamic_pointer_cast<Tensor>(data);
 }
 
 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());
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "{} 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());
+        AIDGE_THROW_OR_ABORT(std::runtime_error, "{} Operator has {} outputs", type().c_str(), nbOutputs());
     }
     *mOutputs[outputIdx] = std::move(*std::dynamic_pointer_cast<Tensor>(data));
 }
 
 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_THROW_OR_ABORT(std::runtime_error, "{} Operator has {} outputs", type().c_str(), nbOutputs());
     }
     return mOutputs[outputIdx];
 }
@@ -105,7 +105,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
diff --git a/src/scheduler/Scheduler.cpp b/src/scheduler/Scheduler.cpp
index 27900297a987ecfe17edb4ec1651a8779e8090e8..646025d23c95b3c72bf740416f6dc3c7391ad974 100644
--- a/src/scheduler/Scheduler.cpp
+++ b/src/scheduler/Scheduler.cpp
@@ -16,6 +16,8 @@
 #include <set>
 #include <string>
 
+#include <fmt/ranges.h>
+
 #include "aidge/graph/GraphView.hpp"
 #include "aidge/graph/Node.hpp"
 #include "aidge/utils/Types.h"
@@ -63,7 +65,8 @@ void Aidge::SequentialScheduler::generateScheduling(bool verbose) {
     // frozen state.
     std::vector<std::set<std::shared_ptr<Node>>> frozenConsumers;
 
-    std::map<std::shared_ptr<Node>, std::string> namePtrTable = getNodesName(verbose);
+    std::map<std::shared_ptr<Node>, std::string> namePtrTable;
+    if (verbose) namePtrTable = mGraphView->getRankedNodesName("{0} ({1}#{3})");
 
     do {
         // From the current consumers list, check if any prior nodes are needed.
@@ -92,8 +95,17 @@ void Aidge::SequentialScheduler::generateScheduling(bool verbose) {
 
             if (prior.isPrior) {
                 if (verbose) {
-                    printf("\t\trequired producers: %s\n", print(prior.requiredProducers, "%s", [&namePtrTable](auto val){ return namePtrTable[val].c_str(); }).c_str());
-                    printf("\t\tprior consumers: %s\n", print(prior.priorConsumers, "%s", [&namePtrTable](auto val){ return namePtrTable[val].c_str(); }).c_str());
+                    std::vector<std::string> requiredProducersName;
+                    std::transform(prior.requiredProducers.begin(), prior.requiredProducers.end(),
+                        std::back_inserter(requiredProducersName),
+                        [&namePtrTable](auto val){ return namePtrTable[val].c_str(); });
+                    fmt::print("\t\trequired producers: {}\n", requiredProducersName);
+
+                    std::vector<std::string> priorConsumersName;
+                    std::transform(prior.priorConsumers.begin(), prior.priorConsumers.end(),
+                        std::back_inserter(priorConsumersName),
+                        [&namePtrTable](auto val){ return namePtrTable[val].c_str(); });
+                    fmt::print("\t\tprior consumers: {}\n", priorConsumersName);
                 }
 
                 requiredProducers.insert(prior.requiredProducers.cbegin(), prior.requiredProducers.cend());
@@ -261,7 +273,8 @@ void Aidge::SequentialScheduler::forward(bool forwardDims, bool verbose) {
     // Clear previous scheduling results
     mScheduling.clear();
 
-    std::map<std::shared_ptr<Node>, std::string> namePtrTable = getNodesName(verbose);
+    std::map<std::shared_ptr<Node>, std::string> namePtrTable;
+    if (verbose) namePtrTable = mGraphView->getRankedNodesName("{0} ({1}#{3})");
 
     int cpt = 0;
     for (const auto& runnable : mStaticSchedule) {
@@ -286,12 +299,13 @@ void Aidge::SequentialScheduler::saveSchedulingDiagram(const std::string& fileNa
     std::fprintf(fp, "gantt\ndateFormat x\naxisFormat %%Q µs\n\n");
 
     if (!mScheduling.empty()) {
-        std::map<std::shared_ptr<Node>, std::string> namePtrTable = getNodesName(true);
+        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",
-                         namePtrTable[element.node].c_str(),
+                         namePtrTable.find(element.node)->second.c_str(),
                          std::chrono::duration_cast<std::chrono::microseconds>(element.start - globalStart).count(),
                          std::chrono::duration_cast<std::chrono::microseconds>(element.end - globalStart).count());
         }
@@ -353,25 +367,3 @@ Aidge::SequentialScheduler::PriorProducersConsumers Aidge::SequentialScheduler::
     }
     return prior;
 }
-
-std::map<std::shared_ptr<Aidge::Node>, std::string> Aidge::SequentialScheduler::getNodesName(bool verbose) const {
-    std::map<std::shared_ptr<Node>, std::string> namePtrTable;
-
-    if (verbose) {
-        std::map<const std::string, std::size_t> typeCounter;
-
-        for (const std::shared_ptr<Node> &node_ptr : mGraphView->getNodes()) {
-            const std::string currentType = node_ptr->type();
-            if (typeCounter.find(currentType) == typeCounter.end())
-                typeCounter[currentType] = 0;
-            ++typeCounter[currentType];
-
-            namePtrTable[node_ptr] =
-                (node_ptr->name().empty())
-                    ? currentType + "#" + std::to_string(typeCounter[currentType])
-                    : node_ptr->name() + " (" + currentType + "#" + std::to_string(typeCounter[currentType]) + ")";
-        }
-    }
-
-    return namePtrTable;
-}