From fe867fc3febf10b497e98a773f108bdf11d0aaa7 Mon Sep 17 00:00:00 2001 From: Olivier BICHLER <olivier.bichler@cea.fr> Date: Wed, 29 Jan 2025 18:39:02 +0100 Subject: [PATCH 1/5] Added missing binding for Elts struct --- include/aidge/scheduler/ProdConso.hpp | 4 + python_binding/data/pybind_Elts.cpp | 85 +++++++++++++++++++ python_binding/pybind_core.cpp | 2 + python_binding/scheduler/pybind_ProdConso.cpp | 1 + 4 files changed, 92 insertions(+) create mode 100644 python_binding/data/pybind_Elts.cpp diff --git a/include/aidge/scheduler/ProdConso.hpp b/include/aidge/scheduler/ProdConso.hpp index f30e00afa..bc42cb36c 100644 --- a/include/aidge/scheduler/ProdConso.hpp +++ b/include/aidge/scheduler/ProdConso.hpp @@ -34,6 +34,10 @@ public: return std::make_unique<ProdConso>(op, true); } + const Operator& getOperator() const noexcept { + return mOp; + } + /** * @brief Minimum amount of data from a specific input required by the * implementation to be run. diff --git a/python_binding/data/pybind_Elts.cpp b/python_binding/data/pybind_Elts.cpp new file mode 100644 index 000000000..59a8211e2 --- /dev/null +++ b/python_binding/data/pybind_Elts.cpp @@ -0,0 +1,85 @@ +/******************************************************************************** + * Copyright (c) 2023 CEA-List + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + ********************************************************************************/ + +#include <algorithm> // std::transform +#include <cctype> // std::tolower +#include <string> // std::string +#include <vector> + +#include <pybind11/pybind11.h> +#include <pybind11/stl.h> +#include <pybind11/operators.h> + +#include "aidge/data/Elts.hpp" + +namespace py = pybind11; +namespace Aidge { + +template <class T> +void bindEnum(py::module& m, const std::string& name) { + // Define enumeration names for python as lowercase type name + // This defined enum names compatible with basic numpy type + // name such as: float32, flot64, [u]int32, [u]int64, ... + auto python_enum_name = [](const T& type) { + auto str_lower = [](std::string& str) { + std::transform(str.begin(), str.end(), str.begin(), + [](unsigned char c){ + return std::tolower(c); + }); + }; + auto type_name = std::string(Aidge::format_as(type)); + str_lower(type_name); + return type_name; + }; + + // Auto generate enumeration names from lowercase type strings + std::vector<std::string> enum_names; + for (auto type_str : EnumStrings<T>::data) { + auto type = static_cast<T>(enum_names.size()); + auto enum_name = python_enum_name(type); + enum_names.push_back(enum_name); + } + + // Define python side enumeration aidge_core.type + auto e_type = py::enum_<T>(m, name.c_str()); + + // Add enum value for each enum name + for (std::size_t idx = 0; idx < enum_names.size(); idx++) { + e_type.value(enum_names[idx].c_str(), static_cast<T>(idx)); + } + + // Define str() to return the bare enum name value, it allows + // to compare directly for instance str(tensor.type()) + // with str(nparray.type) + e_type.def("__str__", [enum_names](const T& type) { + return enum_names[static_cast<int>(type)]; + }, py::prepend()); +} + +void init_Elts(py::module& m) { + bindEnum<Elts_t::EltType>(m, "EltType"); + m.def("format_as", (const char* (*)(Elts_t::EltType)) &format_as, py::arg("elt")); + + py::class_<Elts_t, std::shared_ptr<Elts_t>>( + m, "Elts_t", py::dynamic_attr()) + .def_static("none_elts", &Elts_t::NoneElts) + .def_static("data_elts", &Elts_t::DataElts, py::arg("data"), py::arg("token") = 1) + .def_static("token_elts", &Elts_t::TokenElts, py::arg("token")) + .def_readwrite("data", &Elts_t::data) + .def_readwrite("token", &Elts_t::token) + .def_readwrite("type", &Elts_t::type) + .def(py::self + py::self) + .def(py::self += py::self) + .def(py::self < py::self) + .def(py::self > py::self); +} + +} // namespace Aidge diff --git a/python_binding/pybind_core.cpp b/python_binding/pybind_core.cpp index 1f35373f3..cc6f0bf25 100644 --- a/python_binding/pybind_core.cpp +++ b/python_binding/pybind_core.cpp @@ -21,6 +21,7 @@ void init_Random(py::module&); void init_Data(py::module&); void init_DataFormat(py::module&); void init_DataType(py::module&); +void init_Elts(py::module&); void init_Database(py::module&); void init_DataProvider(py::module&); void init_Interpolation(py::module&); @@ -112,6 +113,7 @@ void init_Aidge(py::module& m) { init_Data(m); init_DataFormat(m); init_DataType(m); + init_Elts(m); init_Database(m); init_DataProvider(m); init_Interpolation(m); diff --git a/python_binding/scheduler/pybind_ProdConso.cpp b/python_binding/scheduler/pybind_ProdConso.cpp index abd6d5379..547e2258d 100644 --- a/python_binding/scheduler/pybind_ProdConso.cpp +++ b/python_binding/scheduler/pybind_ProdConso.cpp @@ -104,6 +104,7 @@ void init_ProdConso(py::module& m){ .def(py::init<const Operator&, bool>(), py::keep_alive<1, 1>(), py::keep_alive<1, 2>(), py::keep_alive<1,3>()) .def_static("default_model", &ProdConso::defaultModel) .def_static("in_place_model", &ProdConso::inPlaceModel) + .def("get_operator", &ProdConso::getOperator) .def("get_nb_required_data", &ProdConso::getNbRequiredData) .def("get_nb_required_protected", &ProdConso::getNbRequiredProtected) .def("get_required_memory", &ProdConso::getRequiredMemory) -- GitLab From 84fac59710ae9d8b1c0dba6b3edd9b786e6a9a76 Mon Sep 17 00:00:00 2001 From: Olivier BICHLER <olivier.bichler@cea.fr> Date: Wed, 29 Jan 2025 18:39:10 +0100 Subject: [PATCH 2/5] Fix ConstantFolding bug --- src/recipes/ConstantFolding.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/recipes/ConstantFolding.cpp b/src/recipes/ConstantFolding.cpp index 40b0bda76..613393756 100644 --- a/src/recipes/ConstantFolding.cpp +++ b/src/recipes/ConstantFolding.cpp @@ -36,6 +36,7 @@ void Aidge::constantFolding(std::shared_ptr<GraphView> graph) { for (const auto& node : candidates) { bool foldable = true; auto replaceGraph = std::make_shared<GraphView>(); + size_t i = 0; for (const auto& input : node->inputs()) { if (input.first) { if (input.first->type() != Producer_Op::Type) { @@ -53,6 +54,13 @@ void Aidge::constantFolding(std::shared_ptr<GraphView> graph) { replaceGraph->add(input.first, false); } + else if (node->inputCategory(i) != InputCategory::OptionalData + && node->inputCategory(i) != InputCategory::OptionalParam) + { + foldable = false; + break; + } + ++i; } if (foldable) { -- GitLab From a0fb5ecd7249789b50554fcc026a91dd6f21a9cd Mon Sep 17 00:00:00 2001 From: Olivier BICHLER <olivier.bichler@cea.fr> Date: Thu, 30 Jan 2025 15:55:03 +0100 Subject: [PATCH 3/5] Added getFactorizedScheduling() --- include/aidge/scheduler/Scheduler.hpp | 12 ++ python_binding/scheduler/pybind_Scheduler.cpp | 1 + src/scheduler/Scheduler.cpp | 118 ++++++++++++++++++ 3 files changed, 131 insertions(+) diff --git a/include/aidge/scheduler/Scheduler.hpp b/include/aidge/scheduler/Scheduler.hpp index 51f62ed1b..75fc15778 100644 --- a/include/aidge/scheduler/Scheduler.hpp +++ b/include/aidge/scheduler/Scheduler.hpp @@ -187,6 +187,7 @@ public: * @param fileName Name of the file to save the diagram (without extension). */ void saveStaticSchedulingDiagram(const std::string& fileName) const; + void saveFactorizedStaticSchedulingDiagram(const std::string& fileName) const; /** * @brief Save in a Mermaid file the order of layers execution. @@ -233,6 +234,17 @@ protected: */ void generateEarlyLateScheduling(std::vector<StaticSchedulingElement*>& schedule) const; + /** + * @brief Get the factorized scheduling, by identifying repetitive sequences + * in the scheduling. + * + * @param schedule Vector of shared pointers to StaticSchedulingElements to be processed + * @return Vector containing the repetitive sequences, in order. The second + * element of the pair is the number of repetitions. + */ + std::vector<std::pair<std::vector<StaticSchedulingElement*>, size_t>> + getFactorizedScheduling(const std::vector<StaticSchedulingElement*>& schedule) const; + private: /** * @brief Summarize the consumer state of a node for debugging purposes. diff --git a/python_binding/scheduler/pybind_Scheduler.cpp b/python_binding/scheduler/pybind_Scheduler.cpp index d4cd7da44..10dc66ae8 100644 --- a/python_binding/scheduler/pybind_Scheduler.cpp +++ b/python_binding/scheduler/pybind_Scheduler.cpp @@ -32,6 +32,7 @@ void init_Scheduler(py::module& m){ .def("graph_view", &Scheduler::graphView) .def("save_scheduling_diagram", &Scheduler::saveSchedulingDiagram, py::arg("file_name")) .def("save_static_scheduling_diagram", &Scheduler::saveStaticSchedulingDiagram, py::arg("file_name")) + .def("save_factorized_static_scheduling_diagram", &Scheduler::saveFactorizedStaticSchedulingDiagram, py::arg("file_name")) .def("resetScheduling", &Scheduler::resetScheduling) .def("generate_scheduling", &Scheduler::generateScheduling) .def("get_static_scheduling", &Scheduler::getStaticScheduling, py::arg("step") = 0, py::arg("sorting") = Scheduler::EarlyLateSort::Default) diff --git a/src/scheduler/Scheduler.cpp b/src/scheduler/Scheduler.cpp index 396e90c09..58c23f708 100644 --- a/src/scheduler/Scheduler.cpp +++ b/src/scheduler/Scheduler.cpp @@ -427,6 +427,82 @@ void Aidge::Scheduler::generateEarlyLateScheduling(std::vector<StaticSchedulingE } } +std::vector<std::pair<std::vector<Aidge::Scheduler::StaticSchedulingElement*>, size_t>> +Aidge::Scheduler::getFactorizedScheduling(const std::vector<StaticSchedulingElement*>& schedule) const +{ + std::vector<std::pair<std::vector<StaticSchedulingElement*>, size_t>> sequences; + size_t offset = 0; + + for (size_t i = 0; i < schedule.size(); ) { + std::vector<StaticSchedulingElement*> seq; + seq.push_back(new StaticSchedulingElement( + schedule[i]->node, + schedule[i]->early - offset, + schedule[i]->late - offset)); + + // Find all the possible repetitive sequences starting from this element + std::vector<std::pair<std::vector<StaticSchedulingElement*>, size_t>> longuestSeq = {std::make_pair(seq, 1)}; + std::vector<size_t> longuestSeqOffset = {0}; + + for (size_t k = i + 1; k < schedule.size() - 1; ++k) { + // For each sequence length, starting from 2... + seq.push_back(new StaticSchedulingElement( + schedule[k]->node, + schedule[k]->early - offset, + schedule[k]->late - offset)); + + size_t start = k + 1; + size_t nbRepeats = 1; + bool repeat = true; + const auto seqOffset = schedule[start]->early - offset - seq[0]->early; + + do { + // Count the number of consecutive sequences (repetitions) + for (size_t r = 0; r < seq.size(); ++r) { + if (start + r >= schedule.size() + || schedule[start + r]->node != seq[r]->node + || schedule[start + r]->early - offset != seq[r]->early + seqOffset * nbRepeats + || schedule[start + r]->late - offset != seq[r]->late + seqOffset * nbRepeats) + { + repeat = false; + break; + } + } + + if (repeat) { + start += seq.size(); + ++nbRepeats; + } + } + while (repeat); + + if (nbRepeats > 1) { + // If repetitions exist for this sequence length, add it to the list + longuestSeq.push_back(std::make_pair(seq, nbRepeats)); + longuestSeqOffset.push_back(seqOffset); + } + } + + // Select the one with the best factorization + // i.e. which maximize the product sequence length * number of sequences + size_t maxS = 0; + size_t maxFactorization = 0; + for (size_t s = 0; s < longuestSeq.size(); ++s) { + const auto factor = longuestSeq[s].first.size() * longuestSeq[s].second; + if (factor > maxFactorization) { + maxFactorization = factor; + maxS = s; + } + } + + sequences.push_back(longuestSeq[maxS]); + i += longuestSeq[maxS].first.size() * longuestSeq[maxS].second; + offset += longuestSeqOffset[maxS] * (longuestSeq[maxS].second - 1); + } + + return sequences; +} + void Aidge::Scheduler::resetScheduling() { for (auto node : mGraphView->getNodes()) { node->getOperator()->resetConsummerProducer(); @@ -878,6 +954,48 @@ void Aidge::Scheduler::saveStaticSchedulingDiagram(const std::string& fileName) fmt::print(fp.get(), "\n"); } +void Aidge::Scheduler::saveFactorizedStaticSchedulingDiagram(const std::string& fileName) const { + auto fp = std::unique_ptr<FILE, decltype(&std::fclose)>(std::fopen((fileName + ".mmd").c_str(), "w"), &std::fclose); + + if (!fp) { + AIDGE_THROW_OR_ABORT(std::runtime_error, + "Could not create scheduling diagram log file: {}", fileName + ".mmd"); + } + + fmt::print(fp.get(), "gantt\ndateFormat x\naxisFormat %Q\n\n"); + + if (!mStaticSchedule.empty()) { + const std::map<std::shared_ptr<Node>, std::string> namePtrTable + = mGraphView->getRankedNodesName("{0} ({1}#{3})"); + + for (const auto& schedule : mStaticSchedule) { + const auto factorizedSchedule = getFactorizedScheduling(schedule); + + size_t seq = 0; + for (const auto& sequence : factorizedSchedule) { + if (sequence.second > 1) { + fmt::print(fp.get(), "section seq#{} (x{})\n", seq, sequence.second); + } + else { + fmt::print(fp.get(), "section seq#{}\n", seq); + } + + for (const auto& element : sequence.first) { + auto name = namePtrTable.at(element->node); + // Mermaid does not allow : character in task title + std::replace(name.begin(), name.end(), ':', '_'); + + fmt::print(fp.get(), "{} :{}, {}\n", + name, element->early, element->late); + } + ++seq; + } + } + } + + fmt::print(fp.get(), "\n"); +} + std::vector<std::shared_ptr<Aidge::Node>> Aidge::Scheduler::getStaticScheduling(std::size_t step, EarlyLateSort sorting) const { AIDGE_ASSERT(!mStaticSchedule.empty(), "Scheduler::getStaticScheduling(): static scheduling is empty, did you generate scheduling first?"); AIDGE_ASSERT(step < mStaticSchedule.size(), "Scheduler::getStaticScheduling(): no static scheduling at step {} (available steps: {})", mStaticSchedule.size(), step); -- GitLab From f222cd7c8de37f6c0fca5eb0bc2088f5231fe60c Mon Sep 17 00:00:00 2001 From: Olivier BICHLER <olivier.bichler@cea.fr> Date: Thu, 30 Jan 2025 16:07:07 +0100 Subject: [PATCH 4/5] Improved display --- src/scheduler/Scheduler.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/scheduler/Scheduler.cpp b/src/scheduler/Scheduler.cpp index 58c23f708..26bb357e7 100644 --- a/src/scheduler/Scheduler.cpp +++ b/src/scheduler/Scheduler.cpp @@ -945,8 +945,14 @@ void Aidge::Scheduler::saveStaticSchedulingDiagram(const std::string& fileName) // Mermaid does not allow : character in task title std::replace(name.begin(), name.end(), ':', '_'); - fmt::print(fp.get(), "{} :{}, {}\n", - name, element->early, element->late); + if (element->early == element->late) { + fmt::print(fp.get(), "{} :milestone, {}, {}\n", + name, element->early, element->late); + } + else { + fmt::print(fp.get(), "{} :{}, {}\n", + name, element->early, element->late); + } } } } @@ -985,8 +991,14 @@ void Aidge::Scheduler::saveFactorizedStaticSchedulingDiagram(const std::string& // Mermaid does not allow : character in task title std::replace(name.begin(), name.end(), ':', '_'); - fmt::print(fp.get(), "{} :{}, {}\n", - name, element->early, element->late); + if (element->early == element->late) { + fmt::print(fp.get(), "{} :milestone, {}, {}\n", + name, element->early, element->late); + } + else { + fmt::print(fp.get(), "{} :{}, {}\n", + name, element->early, element->late); + } } ++seq; } -- GitLab From 1c5925053973884351e5040ec42b7bdb12dd57bc Mon Sep 17 00:00:00 2001 From: Olivier BICHLER <olivier.bichler@cea.fr> Date: Thu, 30 Jan 2025 17:47:58 +0100 Subject: [PATCH 5/5] Small refinement in factorize --- include/aidge/scheduler/Scheduler.hpp | 5 ++- python_binding/scheduler/pybind_Scheduler.cpp | 2 +- src/scheduler/Scheduler.cpp | 44 ++++++++++--------- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/include/aidge/scheduler/Scheduler.hpp b/include/aidge/scheduler/Scheduler.hpp index 75fc15778..db9b903cc 100644 --- a/include/aidge/scheduler/Scheduler.hpp +++ b/include/aidge/scheduler/Scheduler.hpp @@ -187,7 +187,7 @@ public: * @param fileName Name of the file to save the diagram (without extension). */ void saveStaticSchedulingDiagram(const std::string& fileName) const; - void saveFactorizedStaticSchedulingDiagram(const std::string& fileName) const; + void saveFactorizedStaticSchedulingDiagram(const std::string& fileName, size_t minRepeat = 2) const; /** * @brief Save in a Mermaid file the order of layers execution. @@ -239,11 +239,12 @@ protected: * in the scheduling. * * @param schedule Vector of shared pointers to StaticSchedulingElements to be processed + * @param size_t Minimum number repetitions to factorize the sequence * @return Vector containing the repetitive sequences, in order. The second * element of the pair is the number of repetitions. */ std::vector<std::pair<std::vector<StaticSchedulingElement*>, size_t>> - getFactorizedScheduling(const std::vector<StaticSchedulingElement*>& schedule) const; + getFactorizedScheduling(const std::vector<StaticSchedulingElement*>& schedule, size_t minRepeat = 2) const; private: /** diff --git a/python_binding/scheduler/pybind_Scheduler.cpp b/python_binding/scheduler/pybind_Scheduler.cpp index 10dc66ae8..1a3a4b6b2 100644 --- a/python_binding/scheduler/pybind_Scheduler.cpp +++ b/python_binding/scheduler/pybind_Scheduler.cpp @@ -32,7 +32,7 @@ void init_Scheduler(py::module& m){ .def("graph_view", &Scheduler::graphView) .def("save_scheduling_diagram", &Scheduler::saveSchedulingDiagram, py::arg("file_name")) .def("save_static_scheduling_diagram", &Scheduler::saveStaticSchedulingDiagram, py::arg("file_name")) - .def("save_factorized_static_scheduling_diagram", &Scheduler::saveFactorizedStaticSchedulingDiagram, py::arg("file_name")) + .def("save_factorized_static_scheduling_diagram", &Scheduler::saveFactorizedStaticSchedulingDiagram, py::arg("file_name"), py::arg("min_repeat") = 2) .def("resetScheduling", &Scheduler::resetScheduling) .def("generate_scheduling", &Scheduler::generateScheduling) .def("get_static_scheduling", &Scheduler::getStaticScheduling, py::arg("step") = 0, py::arg("sorting") = Scheduler::EarlyLateSort::Default) diff --git a/src/scheduler/Scheduler.cpp b/src/scheduler/Scheduler.cpp index 26bb357e7..bbbc3d807 100644 --- a/src/scheduler/Scheduler.cpp +++ b/src/scheduler/Scheduler.cpp @@ -428,24 +428,19 @@ void Aidge::Scheduler::generateEarlyLateScheduling(std::vector<StaticSchedulingE } std::vector<std::pair<std::vector<Aidge::Scheduler::StaticSchedulingElement*>, size_t>> -Aidge::Scheduler::getFactorizedScheduling(const std::vector<StaticSchedulingElement*>& schedule) const +Aidge::Scheduler::getFactorizedScheduling(const std::vector<StaticSchedulingElement*>& schedule, size_t minRepeat) const { std::vector<std::pair<std::vector<StaticSchedulingElement*>, size_t>> sequences; size_t offset = 0; for (size_t i = 0; i < schedule.size(); ) { - std::vector<StaticSchedulingElement*> seq; - seq.push_back(new StaticSchedulingElement( - schedule[i]->node, - schedule[i]->early - offset, - schedule[i]->late - offset)); - // Find all the possible repetitive sequences starting from this element - std::vector<std::pair<std::vector<StaticSchedulingElement*>, size_t>> longuestSeq = {std::make_pair(seq, 1)}; - std::vector<size_t> longuestSeqOffset = {0}; + std::vector<StaticSchedulingElement*> seq; + std::vector<std::pair<std::vector<StaticSchedulingElement*>, size_t>> longuestSeq; + std::vector<size_t> longuestSeqOffset; - for (size_t k = i + 1; k < schedule.size() - 1; ++k) { - // For each sequence length, starting from 2... + for (size_t k = i; k < schedule.size(); ++k) { + // For each sequence length, starting from 1... seq.push_back(new StaticSchedulingElement( schedule[k]->node, schedule[k]->early - offset, @@ -454,7 +449,7 @@ Aidge::Scheduler::getFactorizedScheduling(const std::vector<StaticSchedulingElem size_t start = k + 1; size_t nbRepeats = 1; bool repeat = true; - const auto seqOffset = schedule[start]->early - offset - seq[0]->early; + const auto seqOffset = (start < schedule.size()) ? schedule[start]->early - offset - seq[0]->early : 0; do { // Count the number of consecutive sequences (repetitions) @@ -476,11 +471,17 @@ Aidge::Scheduler::getFactorizedScheduling(const std::vector<StaticSchedulingElem } while (repeat); - if (nbRepeats > 1) { + if (nbRepeats >= minRepeat) { // If repetitions exist for this sequence length, add it to the list longuestSeq.push_back(std::make_pair(seq, nbRepeats)); longuestSeqOffset.push_back(seqOffset); } + else if (k == i) { + // Ensure that at least the current element is in the list if no + // repetition is found + longuestSeq.push_back(std::make_pair(seq, 1)); + longuestSeqOffset.push_back(0); + } } // Select the one with the best factorization @@ -960,7 +961,7 @@ void Aidge::Scheduler::saveStaticSchedulingDiagram(const std::string& fileName) fmt::print(fp.get(), "\n"); } -void Aidge::Scheduler::saveFactorizedStaticSchedulingDiagram(const std::string& fileName) const { +void Aidge::Scheduler::saveFactorizedStaticSchedulingDiagram(const std::string& fileName, size_t minRepeat) const { auto fp = std::unique_ptr<FILE, decltype(&std::fclose)>(std::fopen((fileName + ".mmd").c_str(), "w"), &std::fclose); if (!fp) { @@ -975,7 +976,7 @@ void Aidge::Scheduler::saveFactorizedStaticSchedulingDiagram(const std::string& = mGraphView->getRankedNodesName("{0} ({1}#{3})"); for (const auto& schedule : mStaticSchedule) { - const auto factorizedSchedule = getFactorizedScheduling(schedule); + const auto factorizedSchedule = getFactorizedScheduling(schedule, minRepeat); size_t seq = 0; for (const auto& sequence : factorizedSchedule) { @@ -990,15 +991,18 @@ void Aidge::Scheduler::saveFactorizedStaticSchedulingDiagram(const std::string& auto name = namePtrTable.at(element->node); // Mermaid does not allow : character in task title std::replace(name.begin(), name.end(), ':', '_'); + std::string tag = ":"; if (element->early == element->late) { - fmt::print(fp.get(), "{} :milestone, {}, {}\n", - name, element->early, element->late); + tag += "milestone, "; } - else { - fmt::print(fp.get(), "{} :{}, {}\n", - name, element->early, element->late); + + if (sequence.second > 1) { + tag += "active, "; } + + fmt::print(fp.get(), "{} {}{}, {}\n", + name, tag, element->early, element->late); } ++seq; } -- GitLab