diff --git a/Tests/heuristics/RelaxationPolicy.cpp b/Tests/heuristics/RelaxationPolicy.cpp index 6a99e53889e0a75a2cfc7270b062dd2e2a9ae81d..9658091b0d9e5d827bfda60b7bc291f4f3e0063a 100644 --- a/Tests/heuristics/RelaxationPolicy.cpp +++ b/Tests/heuristics/RelaxationPolicy.cpp @@ -45,22 +45,23 @@ auto makeTestTaskMap() { TEST(relaxation_policies, TaskVarMap_mapping) { using namespace tempo; using B = BooleanVar<int>; + using namespace std::views; auto [map, tasks] = makeTestTaskMap(); - EXPECT_EQ(map(tasks[0]), std::vector{B(4)}); - EXPECT_EQ(map(tasks[1]), (std::vector{B(4), B(9), B(18)})); - EXPECT_EQ(map(tasks[2]), (std::vector{B(8), B(17)})); - EXPECT_EQ(map(tasks[3]), std::vector{B(9)}); - EXPECT_EQ(map(tasks[4]), (std::vector{B(16), B(21)})); + EXPECT_TRUE(tempo::testing::equalRanges(map(tasks[0]) | elements<1>, std::vector{B(4)})); + EXPECT_TRUE(tempo::testing::equalRanges(map(tasks[1]) | elements<1>, (std::vector{B(4), B(9), B(18)}))); + EXPECT_TRUE(tempo::testing::equalRanges(map(tasks[2]) | elements<1>, (std::vector{B(8), B(17)}))); + EXPECT_TRUE(tempo::testing::equalRanges(map(tasks[3]) | elements<1>, std::vector{B(9)})); + EXPECT_TRUE(tempo::testing::equalRanges(map(tasks[4]) | elements<1>, (std::vector{B(16), B(21)}))); } TEST(relaxation_policies, TaskVarMap_getTaskLiterals) { using namespace tempo; using B = BooleanVar<int>; auto [map, tasks] = makeTestTaskMap(); - auto variables = map.getTaskLiterals(std::array{tasks[2], tasks[3]}); + auto variables = map.getTaskLiterals(std::array{tasks[2], tasks[3]}, true); EXPECT_EQ(variables, (std::vector{B(8), B(9), B(17)})); - variables = map.getTaskLiterals(std::array{tasks[0], tasks[3], tasks[4]}); + variables = map.getTaskLiterals(std::array{tasks[0], tasks[3], tasks[4]}, true); EXPECT_EQ(variables, (std::vector{B(4), B(9), B(16), B(21)})); - variables = map.getTaskLiterals(std::array{tasks[0], tasks[1], tasks[3]}); + variables = map.getTaskLiterals(std::array{tasks[0], tasks[1], tasks[3]}, true); EXPECT_EQ(variables, (std::vector{B(4), B(9), B(18)})); } \ No newline at end of file diff --git a/Tests/testing.hpp b/Tests/testing.hpp index 8b3c5c679edd8adcedf869849f067bfd0574607a..36902e176c14f6ad102284f3087432e7f5f8d98e 100644 --- a/Tests/testing.hpp +++ b/Tests/testing.hpp @@ -15,6 +15,7 @@ #include "util/Matrix.hpp" #include "util/serialization.hpp" +#include "util/printing.hpp" #include "Global.hpp" #include "util/SchedulingProblemHelper.hpp" #include "Model.hpp" @@ -119,6 +120,18 @@ namespace tempo::testing { return dist(el); } + template<std::ranges::range Range1, std::ranges::range Range2> + bool equalRanges(const Range1 &range1, const Range2 &range2) { + if (not std::ranges::equal(range1, range2)) { + std::cout << "ranges not equal:" << std::endl; + printRange(range1, std::cout) << std::endl << "vs" << std::endl; + printRange(range2, std::cout) << std::endl; + return false; + } + + return true; + } + namespace heuristics { struct LitProvider { struct Storage { diff --git a/Tests/util/Lookup.cpp b/Tests/util/Lookup.cpp new file mode 100644 index 0000000000000000000000000000000000000000..96eb4cd63d9b9dfa34522a1742fa172b9c2e092d --- /dev/null +++ b/Tests/util/Lookup.cpp @@ -0,0 +1,77 @@ +/** +* @author Tim Luchterhand +* @date 09.01.25 +* @file Lookup.cpp +* @brief +*/ + +#include <gtest/gtest.h> +#include <Iterators.hpp> + +#include "util/Lookup.hpp" +#include "Literal.hpp" + +TEST(util, Lookup_default_ctor) { + using namespace tempo; + Lookup<int, int> lookup; + EXPECT_EQ(lookup.size(), 0); + EXPECT_EQ(lookup.keyOffset(), 0); + EXPECT_THROW(lookup.at(0), std::out_of_range); +} + +TEST(util, Lookup_ctor) { + using namespace tempo; + auto keys = {3, 1, 7}; + std::vector values{9, 2, 1}; + Lookup lookup(keys, 0, values); + EXPECT_EQ(lookup.keyOffset(), 1); + EXPECT_EQ(lookup.maxKey(), 7); + EXPECT_EQ(lookup.size(), 7); + EXPECT_EQ(lookup.data(), (std::vector{2, 0, 9, 0, 0, 0, 1})); +} + +TEST(util, Lookup_ctor1) { + using namespace tempo; + auto keys = {3, 1, 7}; + Lookup lookup(keys, 17); + EXPECT_EQ(lookup.keyOffset(), 1); + EXPECT_EQ(lookup.maxKey(), 7); + EXPECT_EQ(lookup.size(), 7); + for (auto v : lookup.data()) { + EXPECT_EQ(v, 17); + } +} + +template<typename L, typename Keys, typename Values, typename Key> +void testAccess(L &lookup, const Keys &keys, const Values &values, Key k1, Key k2) { + for (auto [k, v] : iterators::const_zip(keys, values)) { + EXPECT_EQ(lookup.at(k), v); + EXPECT_TRUE(lookup.contains(k)); + EXPECT_EQ(lookup[k], v); + } + + lookup.at(k1) = 19; + EXPECT_EQ(lookup.at(k1), 19); + ASSERT_TRUE(lookup.contains(k2)); + lookup[k2] = -3; + EXPECT_EQ(lookup[k2], -3); +} + +TEST(util, Lookup_access) { + using namespace tempo; + auto keys = {3, 1, 7}; + std::vector values{9, 2, 1}; + Lookup lookup(keys, 0, values); + testAccess(lookup, keys, values, 4, 2); +} + +TEST(util, Lookup_projection) { + using namespace tempo; + auto literals = { + makeBooleanLiteral<int>(true, 4), makeBooleanLiteral<int>(false, 4), makeBooleanLiteral<int>(true, 8) + }; + + auto values = {9, 2, 1}; + Lookup lookup(literals, 0, values, {}, IdProjection{}); + testAccess(lookup, literals, values, makeBooleanLiteral<int>(true, 5), makeBooleanLiteral<int>(false, 7)); +} \ No newline at end of file diff --git a/src/examples/helpers/scheduling_helpers.hpp b/src/examples/helpers/scheduling_helpers.hpp index 43feb5b3569bd22fd619f668e1e2e1c1bbbd4b5b..273ab98615dc964d39ed49af9d09625a4ab852e4 100644 --- a/src/examples/helpers/scheduling_helpers.hpp +++ b/src/examples/helpers/scheduling_helpers.hpp @@ -19,6 +19,7 @@ #include "util/SchedulingProblemHelper.hpp" #include "util/serialization.hpp" #include "util/factory_pattern.hpp" +#include "util/printing.hpp" #include "Solution.hpp" #include "Model.hpp" #include "heuristics/LNS/RelaxationEvaluator.hpp" @@ -144,24 +145,6 @@ auto toSolution(const tempo::serialization::Solution<T> &solution, return tempo::Solution(*problem.solver); } -template<std::ranges::range R> -auto printRange(const R &range, std::ostream &os) -> std::ostream& { - os << "["; - bool first = true; - for (const auto &elem : range) { - if (first) { - first = false; - } else { - os << ", "; - } - os << elem; - } - - os << "]"; - return os; -} - - /** * @tparam Policy LNS relaxation policy type * @tparam T timing type diff --git a/src/examples/testlns.cpp b/src/examples/testlns.cpp index b69986730cf7d29f82b0e5e4e0bbb4162740563e..e78fa20c14f6ab54815b8e053302e7df0a7ad302 100644 --- a/src/examples/testlns.cpp +++ b/src/examples/testlns.cpp @@ -120,7 +120,9 @@ int main(int argc, char *argv[]) { namespace lns = tempo::lns; auto parser = tempo::getBaseParser(); bool profileHeuristic; - lns::RelaxationPolicyParams policyParams{.decayConfig = lns::PolicyDecayConfig(), .numScheduleSlices = 4}; + lns::RelaxationPolicyParams policyParams{ + .decayConfig = lns::PolicyDecayConfig(), .numScheduleSlices = 4, .allTaskEdges = false + }; lns::RelaxPolicy policyType; std::string optSolutionLoc; bool useOracle = false; @@ -144,6 +146,9 @@ int main(int argc, char *argv[]) { policyParams.decayConfig.decayMode), cli::ArgSpec("relax-slices", "number of schedule slices", false, policyParams.numScheduleSlices, 4), + cli::SwitchSpec("fix-all-task-edges", + "whether to fix all task edges or only those between fixed tasks", + policyParams.allTaskEdges, false), cli::ArgSpec("lns-policy", "lns relaxation policy", true, policyType), cli::ArgSpec("optimal-solution", "location of optimal solution (e.g. for oracle)", false, optSolutionLoc), @@ -152,9 +157,6 @@ int main(int argc, char *argv[]) { cli::ArgSpec("sporadic-increment", "sporadic root search probability increment", false, sporadicIncrement)); - std::string ordering_file{""}; - parser.getCmdLine().add<TCLAP::ValueArg<std::string>>(ordering_file, "", "static-ordering", "use static ordering heuristic", false, "", "string"); - parser.parse(argc, argv); Options opt = parser.getOptions(); Solver<> S(opt); @@ -287,8 +289,8 @@ int main(int argc, char *argv[]) { if(not optimal) { MinimizationObjective<int> objective(schedule.duration); if (not useOracle) { - auto policy = lns::make_relaxation_policy(policyType, intervals, resources, policyParams, opt.verbosity); std::cout << "-- using relaxation policy " << policyType << std::endl; + auto policy = lns::make_relaxation_policy(policyType, intervals, resources, policyParams, opt.verbosity); if (sporadicIncrement != 0) { std::cout << "-- root search probability increment " << sporadicIncrement << std::endl; runLNS(lns::make_sporadic_root_search(sporadicIncrement, std::move(policy)), optSolutionLoc, S, diff --git a/src/examples/torch/lns_gnn.cpp b/src/examples/torch/lns_gnn.cpp index 55fcef2f8e4d4870e9082aa91b0a53e628dab401..57e4baea32ebcefe4590601ed28cb810e98a9406 100644 --- a/src/examples/torch/lns_gnn.cpp +++ b/src/examples/torch/lns_gnn.cpp @@ -33,7 +33,7 @@ int main(int argc, char **argv) { std::string featureExtractorConf; lns::PolicyDecayConfig config; lns::AssumptionMode assumptionMode = lns::AssumptionMode::GreedySkip; - lns::RelaxationPolicyParams destroyParameters{.decayConfig = {}, .numScheduleSlices = 4}; + lns::RelaxationPolicyParams destroyParameters{.decayConfig = {}, .numScheduleSlices = 4, .allTaskEdges = false}; destroyParameters.decayConfig.fixRatio = 0.1; destroyParameters.decayConfig.decay = 1; lns::RelaxPolicy destroyType = lns::RelaxPolicy::RandomTasks; @@ -68,6 +68,9 @@ int main(int argc, char **argv) { destroyParameters.decayConfig.decay), cli::ArgSpec("num-slices", "number of schedule slices", false, destroyParameters.numScheduleSlices), + cli::SwitchSpec("fix-all-task-edges", + "whether to fix all task edges or only those between fixed tasks", + destroyParameters.allTaskEdges, false), cli::ArgSpec("sporadic-increment", "probability increment on fail for root search", false, sporadicIncrement), cli::ArgSpec("fix-ratio", "percentage of literals to relax", false, @@ -114,7 +117,8 @@ int main(int argc, char **argv) { if (not optimal and useDRPolicy) { std::cout << "-- exhaustion probability " << exhaustionProbability << std::endl; nn::GNNRepair gnnRepair(*problemInfo.solver, gnnLocation, featureExtractorConf, problemInfo.instance, - config, assumptionMode, minCertainty, exhaustionThreshold, sampleSmoothingFactor); + config, assumptionMode, minCertainty, exhaustionThreshold, sampleSmoothingFactor, + problemInfo.constraints); lns::GenericDestroyPolicy<Time, RP> destroy( lns::make_relaxation_policy(destroyType, problemInfo.instance.tasks(), problemInfo.constraints, destroyParameters)); @@ -126,7 +130,7 @@ int main(int argc, char **argv) { } else if (not optimal) { nn::GNNRelax policy(*problemInfo.solver, gnnLocation, featureExtractorConf, problemInfo.instance, config, assumptionMode, exhaustionThreshold, exhaustionProbability, reuseSolutions, - sampleSmoothingFactor); + sampleSmoothingFactor,problemInfo.constraints); elapsedTime = runLNS(policy, optSol, *problemInfo.solver, objective); } diff --git a/src/header/heuristics/LNS/fix_policies.hpp b/src/header/heuristics/LNS/fix_policies.hpp index 93140508d563de4f766f96e028db516a54b6099d..cb83ea5c9bc814aafff31681c81a5f52eb974811 100644 --- a/src/header/heuristics/LNS/fix_policies.hpp +++ b/src/header/heuristics/LNS/fix_policies.hpp @@ -19,10 +19,11 @@ #include "relaxation_policies.hpp" #include "Solver.hpp" #include "util/random.hpp" +#include "util/Lookup.hpp" namespace tempo::lns { - PENUM(AssumptionMode, BestN, GreedySkip, GreedyInverse, Sample, Optimal) + PENUM(AssumptionMode, BestN, GreedySkip, GreedyInverse, Sample, Optimal, TaskFull, TaskReduced) /** * @brief Literal ordering type. Literals are ordered by their weight either in ascending or descending order or @@ -44,7 +45,7 @@ namespace tempo::lns { constexpr void reset() noexcept {}; template<concepts::scalar T, assumption_interface AI, concepts::scalar C> - std::size_t select(AI &proxy, std::size_t numLiterals, unsigned, + std::size_t select(AI &proxy, std::size_t numLiterals, bool, const std::vector<std::pair<Literal<T>, C>> & weightedLiterals) { using namespace std::views; switch (OT) { @@ -100,9 +101,9 @@ namespace tempo::lns { } template<assumption_interface AI, concepts::scalar C> - std::size_t select(AI &proxy, std::size_t numLiterals, unsigned numFails, + std::size_t select(AI &proxy, std::size_t numLiterals, bool weightsUpdated, const std::vector<std::pair<Literal<T>, C>> & weightedLiterals) { - if (numFails == 0 and numLiterals <= cache.size()) { + if (not weightsUpdated and numLiterals <= cache.size()) { proxy.makeAssumptions(cache | std::views::take(numLiterals)); return std::min(numLiterals, cache.size()); } @@ -158,7 +159,7 @@ namespace tempo::lns { explicit SampleFix(double smoothingFactor) noexcept: smoothingFactor(smoothingFactor) {} template<concepts::scalar T, assumption_interface AI, std::floating_point C> - std::size_t select(AI &proxy, std::size_t numLiterals, unsigned, + std::size_t select(AI &proxy, std::size_t numLiterals, bool, const std::vector<std::pair<Literal<T>, C>> & weightedLiterals) { using namespace std::views; const auto literals = weightedLiterals | elements<0>; @@ -231,175 +232,201 @@ namespace tempo::lns { template<concepts::scalar T, typename Lookup> auto makeLookup(Solver<T> &solver, const std::vector<BooleanVar<T>> &vars, - std::size_t numLits, Lookup &&lookup) { - return CachedLookup<T, Lookup>(solver, vars, numLits, std::forward<Lookup>(lookup)); + std::size_t numLits, Lookup &&lookup) { + return CachedLookup<T, Lookup>(solver, vars, numLits, std::forward<Lookup>(lookup)); + } } -} - -/** - * @brief Fix policy that tries to fix N edges by maximizing the sum of their confidence values while respecting - * the learned clauses - * @todo integrate problem constraints - * @tparam T timing type - */ -template<concepts::scalar T> -class OptimalFix { - std::vector<Literal<T>> cache; - unsigned timeLimit; - unsigned failLimit; - bool hardTimeLimit; - static constexpr auto FixPointPrec = 1000; //@TODO use Solver<float> -public: /** - * Ctor - * @param timeLimit time limit in ms for optimization problem - * @param failLimit fail limit for optimization problem - * @param hardTimeLimit whether to always cut off after the time limit even when no solution has been found + * @brief Fix policy that tries to fix N edges by maximizing the sum of their confidence values while respecting + * the learned clauses + * @todo integrate problem constraints + * @tparam T timing type */ - OptimalFix(unsigned timeLimit, unsigned failLimit, bool hardTimeLimit) noexcept: timeLimit(timeLimit), - failLimit(failLimit), - hardTimeLimit(hardTimeLimit) {} + template<concepts::scalar T> + class OptimalFix { + std::vector<Literal<T>> cache; + unsigned timeLimit; + unsigned failLimit; + bool hardTimeLimit; + static constexpr auto FixPointPrec = 1000; //@TODO use Solver<float> + public: - void reset() noexcept { - cache.clear(); - } + /** + * Ctor + * @param timeLimit time limit in ms for optimization problem + * @param failLimit fail limit for optimization problem + * @param hardTimeLimit whether to always cut off after the time limit even when no solution has been found + */ + OptimalFix(unsigned timeLimit, unsigned failLimit, bool hardTimeLimit) noexcept: timeLimit(timeLimit), + failLimit(failLimit), + hardTimeLimit(hardTimeLimit) {} - template<assumption_interface AI, concepts::scalar C> - std::size_t select(AI &proxy, std::size_t numLiterals, unsigned numFails, - const std::vector<std::pair<Literal<T>, C>> & weightedLiterals) { - using namespace std::views; - using ST = int; - if (numFails == 0 and not cache.empty()) { - proxy.makeAssumptions(cache | take(numLiterals)); - return std::min(numLiterals, cache.size()); + void reset() noexcept { + cache.clear(); } - cache.clear(); - Options opt; - opt.search_limit = failLimit; - opt.verbosity = Options::SILENT; - if (proxy.getSolver().getOptions().verbosity >= Options::SOLVERINFO) { - std::cout << "-- solving selection problem\n"; - opt.verbosity = Options::NORMAL; - } + template<assumption_interface AI, concepts::scalar C> + std::size_t select(AI &proxy, std::size_t numLiterals, bool weightsUpdated, + const std::vector<std::pair<Literal<T>, C>> & weightedLiterals) { + using namespace std::views; + using ST = int; + if (not weightsUpdated and not cache.empty()) { + proxy.makeAssumptions(cache | take(numLiterals)); + return std::min(numLiterals, cache.size()); + } - tempo::util::StopWatch stopWatch; - bool solution = false; - Solver<ST> solver(opt); - solver.SolutionFound.subscribe_unhandled([&solution, &solver, &stopWatch, this](auto &&) { - solution = true; - if (stopWatch.elapsed<std::chrono::milliseconds>() > timeLimit) { - solver.cancelSearch(); + cache.clear(); + Options opt; + opt.search_limit = failLimit; + opt.verbosity = Options::SILENT; + if (proxy.getSolver().getOptions().verbosity >= Options::SOLVERINFO) { + std::cout << "-- solving selection problem\n"; + opt.verbosity = Options::NORMAL; } - }); - solver.PropagationCompleted.subscribe_unhandled([&stopWatch, &solver, &solution, this](auto &&) { - if ((hardTimeLimit or solution) and stopWatch.elapsed<std::chrono::milliseconds>() > timeLimit) { - solver.cancelSearch(); + tempo::util::StopWatch stopWatch; + bool solution = false; + Solver<ST> solver(opt); + solver.SolutionFound.subscribe_unhandled([&solution, &solver, &stopWatch, this](auto &&) { + solution = true; + if (stopWatch.elapsed<std::chrono::milliseconds>() > timeLimit) { + solver.cancelSearch(); + } + }); + + solver.PropagationCompleted.subscribe_unhandled([&stopWatch, &solver, &solution, this](auto &&) { + if ((hardTimeLimit or solution) and stopWatch.elapsed<std::chrono::milliseconds>() > timeLimit) { + solver.cancelSearch(); + } + }); + std::vector<BooleanVar<ST>> variables; + std::vector<ST> weights; + variables.reserve(weightedLiterals.size()); + weights.reserve(weightedLiterals.size()); + for (auto weight: weightedLiterals | elements<1>) { + auto x = solver.newBoolean(); + solver.addToSearch(x); + variables.emplace_back(x); + weights.emplace_back(static_cast<ST>(weight * FixPointPrec)); } - }); - std::vector<BooleanVar<ST>> variables; - std::vector<ST> weights; - variables.reserve(weightedLiterals.size()); - weights.reserve(weightedLiterals.size()); - for (auto weight: weightedLiterals | elements<1>) { - auto x = solver.newBoolean(); - solver.addToSearch(x); - variables.emplace_back(x); - weights.emplace_back(static_cast<ST>(weight * FixPointPrec)); - } - const auto &cb = proxy.getSolver().clauses; - if (cb.size() != 0) { - auto lookup = detail::makeLookup(solver, variables, proxy.getSolver().numLiteral(), - weightedLiterals | elements<0>); - auto clauses = iota(0ul, cb.size()) | transform([&cb](auto idx) { return cb[idx]; }) | - filter([](auto ptr) { return nullptr != ptr; }); - for (const auto c : clauses) { - auto clause = *c | transform(lookup) | common; - solver.clauses.add(clause.begin(), clause.end()); + const auto &cb = proxy.getSolver().clauses; + if (cb.size() != 0) { + auto lookup = detail::makeLookup(solver, variables, proxy.getSolver().numLiteral(), + weightedLiterals | elements<0>); + auto clauses = iota(0ul, cb.size()) | transform([&cb](auto idx) { return cb[idx]; }) | + filter([](auto ptr) { return nullptr != ptr; }); + for (const auto c : clauses) { + auto clause = *c | transform(lookup) | common; + solver.clauses.add(clause.begin(), clause.end()); + } } - } - solver.post(AtMost(static_cast<ST>(numLiterals), variables)); - auto objective = Sum(variables, weights); - stopWatch.start(); - solver.maximize(objective); - if (not solver.boolean.hasSolution()) { - proxy.fail(); - return 0; - } + solver.post(AtMost(static_cast<ST>(numLiterals), variables)); + auto objective = Sum(variables, weights); + stopWatch.start(); + solver.maximize(objective); + if (not solver.boolean.hasSolution()) { + proxy.fail(); + return 0; + } - for (auto [valid, lit]: iterators::zip( - variables | transform([&solver](const auto &x) { return solver.boolean.value(x); }), - weightedLiterals | elements<0>)) { - if (valid) { - cache.emplace_back(lit); + for (auto [valid, lit]: iterators::zip( + variables | transform([&solver](const auto &x) { return solver.boolean.value(x); }), + weightedLiterals | elements<0>)) { + if (valid) { + cache.emplace_back(lit); + } } - } - proxy.makeAssumptions(cache); - return cache.size(); - } -}; + proxy.makeAssumptions(cache); + return cache.size(); + } + }; - template<concepts::scalar T> + /** + * @brief Fix policy that groups variables by tasks + * @tparam T timing type + * @tparam InvertWeights whether to invert literal weights + */ + template<concepts::scalar T, bool InvertWeights> class TaskFix { - static_assert(traits::always_false_v<T>, "Implementation incomplete, do not use!"); + static constexpr auto DefaultVarsPerTask = 5; std::vector<std::pair<Interval<T>, double>> tasks; - lns::detail::TaskVarMap<T> map; - std::vector<Literal<T>> cache{}; - std::size_t varsPerTask = 0; - std::size_t lastNumTasks = 0; + detail::TaskVarMap<T> map; + bool allTaskEdges; - void calcWeights(const std::vector<std::pair<Literal<T>, double>> & weightedLiterals) { + bool initWeights = true; + double varsPerTask = DefaultVarsPerTask; + std::size_t iterations = 1; + + template<std::floating_point C> + void calcWeights(const std::vector<std::pair<Literal<T>, C>> & weightedLiterals) { + using namespace std::views; + Lookup weights(weightedLiterals | elements<0> | transform([](auto l) { return BooleanVar(l); }), 0.0, + weightedLiterals | elements<1>, {}, + IdProjection{}); for (auto &[t, confidence]: tasks) { confidence = 0; - for (const auto &var : map(t)) { - auto res = std::ranges::find_if(weightedLiterals, var, [&var](const auto &pair) { - return pair.first.variable() == var.id(); - }); - - if (res != weightedLiterals.end()) { - confidence += res->second; + assert(map.contains(t)); + for (const auto &var : map(t) | elements<1>) { + if (weights.contains(var)) { + confidence += weights[var]; } } } - std::ranges::sort(tasks, {}, [](const auto &pair) { return -pair.second; }); + std::ranges::sort(tasks, {}, [](const auto &pair) { return (2 * not InvertWeights - 1) * pair.second; }); } public: - + /** + * Ctor + * @tparam Tasks task range type + * @tparam RR resource range type + * @param tasks tasks in the problem + * @param resources resource expressions in the problem + * @param allTaskEdges whether to fix all task edges + */ template<concepts::typed_range<Interval<T>> Tasks, resource_range RR> - TaskFix(const Tasks &tasks, const RR &resources): map(tasks, resources) { + TaskFix(const Tasks &tasks, const RR &resources, bool allTaskEdges): map(tasks, resources), + allTaskEdges(allTaskEdges) { this->tasks.reserve(std::ranges::size(tasks)); for (const auto &t : tasks) { - varsPerTask += map(t).size(); this->tasks.emplace_back(t, 0.0); } } - void reset() noexcept { - cache.clear(); - } + void reset() noexcept {} - template<assumption_interface AI> - std::size_t select(AI &proxy, std::size_t numLiterals, unsigned numFails, - const std::vector<std::pair<Literal<T>, double>> & weightedLiterals) { - const auto numTasks = numLiterals / varsPerTask; + template<assumption_interface AI, std::floating_point C> + std::size_t select(AI &proxy, std::size_t numLiterals, bool weightsUpdated, + const std::vector<std::pair<Literal<T>, C>> & weightedLiterals) { + using namespace std::views; + const auto numTasks = std::min(static_cast<std::size_t>(numLiterals / varsPerTask), tasks.size()); if (numTasks == 0) { return 0; } - if (lastNumTasks == numTasks and not cache.empty()) { - proxy.makeAssumptions(cache); - return cache.size(); + if (weightsUpdated or initWeights) { + initWeights = false; + calcWeights(weightedLiterals); + if (proxy.getSolver().getOptions().verbosity >= Options::YACKING) { + std::cout << "-- reevaluating task weights" << std::endl; + } } - if (cache.empty()) { - calcWeights(weightedLiterals); + auto vars = map.getTaskLiterals(tasks | elements<0> | take(numTasks), allTaskEdges); + varsPerTask += (1.0 / ++iterations) * (static_cast<double>(vars.size()) / numTasks - varsPerTask); + if (proxy.getSolver().getOptions().verbosity >= Options::YACKING) { + std::cout << "-- fixing " << numTasks << " / " << tasks.size() + << " tasks (" << vars.size() << ") variables" << std::endl; } + + auto literalTransform = [&b = proxy.getSolver().boolean](const auto &var) { return var == b.value(var); }; + proxy.makeAssumptions(vars | transform(literalTransform)); + return vars.size(); + } }; diff --git a/src/header/heuristics/LNS/relaxation_policies.hpp b/src/header/heuristics/LNS/relaxation_policies.hpp index f9855f7e299a0ea56b46eb17527e4a8f6be04028..c74e195b43650d3af47fbc45b0bdc75e893e703c 100755 --- a/src/header/heuristics/LNS/relaxation_policies.hpp +++ b/src/header/heuristics/LNS/relaxation_policies.hpp @@ -34,6 +34,7 @@ #include "util/factory_pattern.hpp" #include "util/random.hpp" #include "heuristics/LNS/PolicyDecay.hpp" +#include "util/Lookup.hpp" namespace tempo::lns { @@ -147,17 +148,16 @@ void RandomSubset<T>::relax(AI &s) const { namespace detail { template<concepts::scalar T> class TaskVarMap { - std::size_t offset = 0; - std::vector<std::vector<BooleanVar<T>>> map{}; + Lookup<Interval<T>, std::vector<std::pair<int, BooleanVar<T>>>, IdProjection> map{}; // target task id, corresponding variable + mutable Lookup<Interval<T>, bool, IdProjection> relaxed; public: template<concepts::typed_range<Interval<T>> Tasks, resource_range RR> TaskVarMap(const Tasks &tasks, const RR &resources) { using namespace std::views; - auto [minT, maxT] = std::ranges::minmax(tasks, {}, [](const auto &t) { return t.id(); }); - offset = minT.id(); - map.resize(maxT.id() - offset + 1); + map = decltype(map)(tasks); + relaxed = decltype(relaxed)(tasks, false, {}, std::pair{map.keyOffset(), map.maxKey()}); for (const auto &t : tasks) { - auto &tVars = map[t.id() - offset]; + auto &tVars = map[t]; for (const auto &r : resources) { auto res = std::ranges::find(r, t); if (res == std::ranges::end(r)) { @@ -165,30 +165,47 @@ namespace detail { } const auto idx = std::ranges::distance(std::ranges::begin(r), res); - auto vars = r.getDisjunctiveLiterals().row_unsafe(idx) | - filter([](auto l) { return l != Contradiction<T>; }) | - transform([](auto l) { return BooleanVar<T>(l); }); + for (auto [targetTask, lit]: iterators::enumerate(r.getDisjunctiveLiterals().row_unsafe(idx))) { + if (lit == Contradiction<T>) { + continue; + } - std::ranges::copy(vars, std::back_inserter(tVars)); + tVars.emplace_back(targetTask, lit); + } } - std::ranges::sort(tVars); - auto res = std::ranges::unique(tVars); + auto comp = [](const auto &pair) { return pair.second.id(); }; + std::ranges::sort(tVars, {}, comp); + auto res = std::ranges::unique(tVars, {}, comp); tVars.erase(res.begin(), res.end()); tVars.shrink_to_fit(); } } + template<concepts::same_template<Interval> Task> + bool contains(const Task &t) const noexcept { + return map.contains(t); + } + template<concepts::same_template<Interval> Task> auto operator()(const Task &t) const noexcept -> const auto & { - return map[t.id() - offset]; + return map[t]; } template<concepts::typed_range<Interval<T>> Tasks> - auto getTaskLiterals(const Tasks &tasks) const -> std::vector<BooleanVar<T>> { + auto getTaskLiterals(const Tasks &tasks, bool allEdges) const -> std::vector<BooleanVar<T>> { using namespace std::views; - auto varsView = - tasks | transform([this](const auto &t) -> decltype(auto) { return (*this)(t); }) | join; + if (not allEdges) { + relaxed.data().assign(relaxed.size(), false); + for (const auto &t : tasks) { + relaxed[t] = true; + } + } + + auto varsView = tasks | transform([this](const auto &t) -> decltype(auto) { return (*this)(t); }) | join + | filter([this, allEdges](const auto &tpl) { + return allEdges or relaxed.data()[std::get<0>(tpl)]; + }) | elements<1>; std::vector<BooleanVar<T>> vars; std::ranges::copy(varsView, std::back_inserter(vars)); std::ranges::sort(vars); @@ -209,6 +226,7 @@ class RelaxTasks { std::vector<Interval<T>> tasks; detail::TaskVarMap<T> map; PolicyDecay decayHandler; + bool allTaskEdges; public: /** * Ctor @@ -216,14 +234,17 @@ public: * @param tasks vector of all tasks to consider * @param resources resource expressions in the problem * @param decayConfig dynamic relaxation ratio decay config + * @param allTaskEdges whether to fix all edges of a task or only these between other fixed tasks * @param verbosity logging verbosity */ template<resource_range RR> RelaxTasks(std::vector<Interval<T>> tasks, const RR &resources, const PolicyDecayConfig &decayConfig, - int verbosity = Options::NORMAL): tasks(std::move(tasks)), map(this->tasks, resources), - decayHandler(decayConfig, map.getTaskLiterals(this->tasks).size(), - verbosity) { + bool allTaskEdges, int verbosity = Options::NORMAL): + tasks(std::move(tasks)), map(this->tasks, resources), + decayHandler(decayConfig,map.getTaskLiterals(this->tasks, allTaskEdges).size(),verbosity), + allTaskEdges(allTaskEdges) { if (verbosity >= Options::YACKING) { + std::cout << std::boolalpha << "-- fix all task edges: " << allTaskEdges << std::noboolalpha << std::endl; std::cout << decayConfig << std::endl; } } @@ -245,7 +266,7 @@ public: } std::ranges::shuffle(tasks, RNG{}); - auto vars = map.getTaskLiterals(counted(tasks.begin(), numFix)); + auto vars = map.getTaskLiterals(counted(tasks.begin(), numFix), allTaskEdges); if (proxy.getSolver().getOptions().verbosity >= Options::YACKING) { std::cout << "-- fixing " << numFix << " / " << tasks.size() << " tasks (" << vars.size() << ") variables" << std::endl; @@ -270,6 +291,7 @@ class RelaxChronologically { unsigned numberOfSlices; unsigned sliceWidth; unsigned sliceIdx = 0; + bool allTaskEdges; public: @@ -279,12 +301,16 @@ public: * @param tasks vector of all tasks to consider * @param resources resource expressions in the problem * @param numberOfSlices number of slices to split the schedule + * @param allTaskEdges whether to fix all edges of a task or only these between other fixed tasks */ template<resource_range RR> - RelaxChronologically(std::vector<Interval<T>> tasks, const RR &resources, unsigned numberOfSlices): - tasks(std::move(tasks)), map(this->tasks, resources), - numberOfSlices(std::max(numberOfSlices, static_cast<unsigned>(tasks.size()))), - sliceWidth(ceil_division(this->tasks.size(), static_cast<std::size_t>(this->numberOfSlices))) { + RelaxChronologically(std::vector<Interval<T>> tasks, const RR &resources, unsigned numberOfSlices, + bool allTaskEdges): tasks(std::move(tasks)), map(this->tasks, resources), + numberOfSlices( + std::max(numberOfSlices, static_cast<unsigned>(tasks.size()))), + sliceWidth(ceil_division(this->tasks.size(), + static_cast<std::size_t>(this->numberOfSlices))), + allTaskEdges(allTaskEdges) { if (numberOfSlices == 0) { throw std::runtime_error("number of slices cannot be 0"); } @@ -313,7 +339,7 @@ public: std::ranges::subrange tRange(tasks.cbegin() + idx * sliceWidth, tasks.cbegin() + std::min((idx + 1) * sliceWidth, tasks.size())); - auto vars = map.getTaskLiterals(tRange); + auto vars = map.getTaskLiterals(tRange, allTaskEdges); bool success = proxy.makeAssumptions(vars | std::views::transform( [&b = proxy.getSolver().boolean](const auto &var) { return var == b.value(var); })); if (not success) { diff --git a/src/header/heuristics/LNS/relaxation_policy_factories.hpp b/src/header/heuristics/LNS/relaxation_policy_factories.hpp index 652a205fcc2de8dcc45b6080c14ef2333edf4681..a104d69356f237813b13bc18dbcd4512c193ae80 100644 --- a/src/header/heuristics/LNS/relaxation_policy_factories.hpp +++ b/src/header/heuristics/LNS/relaxation_policy_factories.hpp @@ -28,6 +28,8 @@ namespace tempo::lns { struct RelaxationPolicyParams { PolicyDecayConfig decayConfig; ///< dynamic relaxation ratio decay config unsigned numScheduleSlices; ///< number of schedule slices for chronological task relaxation + bool allTaskEdges; ///< Whether to fix all edges of a task or only those that connect to other fixed tasks + ///< (only effective when relaxing based on tasks) }; // --- add further relaxation policies to this type --- @@ -59,13 +61,15 @@ namespace tempo::lns { MAKE_TEMPLATE_FACTORY(RandomTasks, ESCAPE(concepts::scalar T, resource_range R), ESCAPE(std::vector<Interval<T>> tasks, R &&resources, const RelaxationPolicyParams ¶ms, int verbosity)) { - return RelaxTasks(std::move(tasks), std::forward<R>(resources), params.decayConfig, verbosity); + return RelaxTasks(std::move(tasks), std::forward<R>(resources), params.decayConfig, + params.allTaskEdges, verbosity); } }; MAKE_TEMPLATE_FACTORY(Chronologically, ESCAPE(concepts::scalar T, resource_range R), ESCAPE(std::vector<Interval<T>> tasks, R &&resources, const RelaxationPolicyParams ¶ms, int)) { - return RelaxChronologically(std::move(tasks), std::forward<R>(resources), params.numScheduleSlices); + return RelaxChronologically(std::move(tasks), std::forward<R>(resources), + params.numScheduleSlices, params.allTaskEdges); } }; diff --git a/src/header/nn/GNNRepair.hpp b/src/header/nn/GNNRepair.hpp index 97ebcd011535e8cab88f5be660b0631ba6d92c45..df6bdab57f110fc0d25830f39f1b5c699848c853 100644 --- a/src/header/nn/GNNRepair.hpp +++ b/src/header/nn/GNNRepair.hpp @@ -40,7 +40,8 @@ namespace tempo::nn { template<concepts::scalar T, SchedulingResource R> class GNNRepair { using FixPolicy = lns::VariantFix<lns::BestN<lns::OrderType::Descending>, - lns::GreedyFix<T, lns::OrderType::Descending>, lns::SampleFix<false>, lns::OptimalFix<T>>; + lns::GreedyFix<T, lns::OrderType::Descending>, lns::SampleFix<false>, + lns::OptimalFix<T>, lns::TaskFix<T, false>>; GNNPrecedencePredictor<T, R> predictor; mutable tempo::util::Profiler profiler{}; std::vector<std::pair<Literal<T>, double>> gnnCache; @@ -50,6 +51,7 @@ namespace tempo::nn { double minCertainty; double exhaustionThreshold; const Solver<T> &solver; + bool newInference = false; public: @@ -76,12 +78,15 @@ namespace tempo::nn { * @param minCertainty minimum GNN certainty * @param exhaustionThreshold fix ratio threshold at witch to signal exhaustion * @param sampleSmoothingFactor smoothing factor for sample fix policy + * @param resourceConstraints Resource expression needed for task fix policy, default empty */ + template<resource_range RR = std::vector<NoOverlapExpression<>>> GNNRepair(const Solver<T> &solver, const fs::path &modelLocation, const fs::path &featureExtractorConfigLocation, const SchedulingProblemHelper<T, R> &problemInstance, const lns::PolicyDecayConfig &decayConfig, lns::AssumptionMode assumptionMode, - double minCertainty, double exhaustionThreshold, double sampleSmoothingFactor = 0) : + double minCertainty, double exhaustionThreshold, double sampleSmoothingFactor = 0, + const RR &resourceConstraints = {}) : predictor(modelLocation, featureExtractorConfigLocation, problemInstance, problemInstance.getSearchLiterals(solver)), policyDecay(decayConfig, predictor.numLiterals(), solver.getOptions().verbosity), @@ -124,6 +129,16 @@ namespace tempo::nn { break; case BestN: break; + case TaskFull: + fixPolicy.template emplace<lns::TaskFix<T, false>>(problemInstance.tasks(), + resourceConstraints, true); + break; + case TaskReduced: + fixPolicy.template emplace<lns::TaskFix<T, false>>(problemInstance.tasks(), + resourceConstraints, false); + break; + default: + throw std::runtime_error("invalid assumption mode " + to_string(assumptionMode)); } } @@ -140,7 +155,8 @@ namespace tempo::nn { } tempo::util::ScopeWatch sw(profiler, "repair"); - numFixed = fixPolicy.select(s, numLits, policyDecay.getFailCount(), gnnCache); + numFixed = fixPolicy.select(s, numLits, newInference, gnnCache); + newInference = false; if (solver.getOptions().verbosity >= Options::YACKING) { if (s.getState() == lns::AssumptionState::Fail) { std::cout << "-- failed to fix literals\n"; @@ -163,6 +179,7 @@ namespace tempo::nn { */ void runInference() { using namespace std::views; + newInference = true; if (maxNumLiterals() == 0) { gnnCache.clear(); return; diff --git a/src/header/nn/gnn_relaxation.hpp b/src/header/nn/gnn_relaxation.hpp index 4496665b1c3ba705a9ed5b78166f8ed035dc5da5..36df5265997191427a972f71787e05dfee8ec7a8 100644 --- a/src/header/nn/gnn_relaxation.hpp +++ b/src/header/nn/gnn_relaxation.hpp @@ -35,7 +35,7 @@ namespace tempo::nn { class GNNRelax { // --- helpers using FixPolicy = lns::VariantFix<lns::BestN<lns::OrderType::Ascending>, - lns::GreedyFix<T, lns::OrderType::Ascending>, lns::SampleFix<true>>; + lns::GreedyFix<T, lns::OrderType::Ascending>, lns::SampleFix<true>, lns::TaskFix<T, true>>; GNNRelaxationPredictor<T, R> predictor; mutable tempo::util::Profiler profiler; FixPolicy fixPolicy; @@ -47,6 +47,7 @@ namespace tempo::nn { std::vector<std::pair<Literal<T>, DataType>> gnnCache; std::size_t numFixed = 0; double qualityFactor = 1; + bool newInference = false; // --- config double exhaustionThreshold; @@ -72,12 +73,14 @@ namespace tempo::nn { * @param exhaustionProbability probability with which a new solution is explored even if not exhausted * @param reuseSolutions whether the same solution may be used twice * @param sampleSmoothingFactor smoothing factor for sample fix policy + * @param resourceConstraints Resource expression needed for task fix policy, default empty */ + template<resource_range RR = std::vector<NoOverlapExpression<>>> GNNRelax(const Solver<T> &solver, const fs::path &modelLocation, const fs::path &featureExtractorConfigLocation, const SchedulingProblemHelper<T, R> &problemInstance, const lns::PolicyDecayConfig &decayConfig, lns::AssumptionMode assumptionMode, double exhaustionThreshold, double exhaustionProbability, bool reuseSolutions = false, - double sampleSmoothingFactor = 0) : + double sampleSmoothingFactor = 0, const RR &resourceConstraints = {}) : predictor(modelLocation, featureExtractorConfigLocation, problemInstance, problemInstance.getSearchLiterals(solver)), policyDecay(decayConfig, predictor.numLiterals(), solver.getOptions().verbosity), @@ -99,6 +102,14 @@ namespace tempo::nn { case Sample: fixPolicy.template emplace<lns::SampleFix<true>>(sampleSmoothingFactor); break; + case TaskFull: + fixPolicy.template emplace<lns::TaskFix<T, true>>(problemInstance.tasks(), + resourceConstraints, true); + break; + case TaskReduced: + fixPolicy.template emplace<lns::TaskFix<T, true>>(problemInstance.tasks(), + resourceConstraints, false); + break; default: throw std::runtime_error("unsupported assumption mode " + to_string(assumptionMode)); } @@ -141,7 +152,8 @@ namespace tempo::nn { } tempo::util::ScopeWatch sw(profiler, "relax"); - numFixed = fixPolicy.select(proxy, maxNumLiterals(), policyDecay.getFailCount(), gnnCache); + numFixed = fixPolicy.select(proxy, maxNumLiterals(), newInference, gnnCache); + newInference = false; if (verbosity >= Options::YACKING) { if (proxy.getState() == lns::AssumptionState::Fail) { std::cout << "-- failed to fix literals\n"; @@ -199,6 +211,7 @@ namespace tempo::nn { bool runInference(const Solver<T> &solver) { using namespace std::views; + newInference = true; if (maxNumLiterals() == 0 or solutions.empty()) { gnnCache.clear(); return false; diff --git a/src/header/util/Lookup.hpp b/src/header/util/Lookup.hpp new file mode 100644 index 0000000000000000000000000000000000000000..88c4e502ce1d87f4e9ef2e83c6d5e7022f787d86 --- /dev/null +++ b/src/header/util/Lookup.hpp @@ -0,0 +1,191 @@ +/** +* @author Tim Luchterhand +* @date 09.01.25 +* @file Lookup.hpp +* @brief Contiguous lookup table +*/ + +#ifndef LOOKUP_HPP +#define LOOKUP_HPP + +#include <ranges> +#include <functional> +#include <vector> +#include <optional> +#include <algorithm> +#include <Iterators.hpp> + +#include "traits.hpp" + + +namespace tempo { + + /** + * Requirement for a key-projection + */ + template<typename P, typename K> + concept key_projection = std::invocable<P, const K &> and + std::integral<std::remove_cvref_t<std::invoke_result_t<P, const K &>>>; + + /** + * @brief Projection functor that gets the id from a given key + */ + struct IdProjection { + template<typename T> + constexpr auto operator()(const T& t) const { + return t.id(); + } + }; + + /** + * @brief Efficient lookup table for non-contiguous key-value pairs + * @details @copybrief + * @tparam K key type + * @tparam V value type + * @tparam P key projection function + * @note this class has a memory overhead if keys are non-contiguous. + */ + template<typename K, typename V, key_projection<K> P = std::identity> + class Lookup { + std::vector<V> table{}; + long offset{0}; + P projection{}; + + template<concepts::typed_range<K> Keys, std::convertible_to<V> Value = V> + void initTable(const Keys& keys, const Value &value = {}) { + if (table.empty()) { + auto [min, max] = std::ranges::minmax(keys, {}, projection); + auto prMin = projection(min); + auto prMax = projection(max); + assert(prMin <= prMax); + assert(prMin >= 0 and prMax >= 0); + table.resize(static_cast<std::size_t>(prMax - prMin) + 1, value); + offset = static_cast<long>(prMin); + } + } + + public: + Lookup() = default; + + /** + * Ctor + * @tparam Keys key range type + * @tparam Values value range type + * @param keys range of keys + * @param defaultValue Value to be inserted into empty places (default: default value of value type) + * @param values optional range of values (if not given will be default initialized) + * @param keyBounds optional pair(lower key bound, upper key bound), both inclusive. If not given, will be + * determined form the key range + * @param projection optional key projection function (default identity) + */ + template<concepts::typed_range<K> Keys, concepts::ctyped_range<V> Values = std::vector<V>> + explicit Lookup(const Keys &keys, const V &defaultValue = {}, const Values &values = {}, + std::optional<std::pair<long, long>> keyBounds = {}, const P &projection = {}) + : table(keyBounds.has_value() ? static_cast<std::size_t>(keyBounds->second - keyBounds->first) : 0, + defaultValue), + offset(keyBounds.has_value() ? keyBounds->first : 0), projection(projection) { + initTable(keys, defaultValue); + for (auto [k, v]: iterators::zip(keys, values)) { + this->at(k) = v; + } + } + + /** + * Value access without bounds checking + * @param key Key + * @return stored value + */ + decltype(auto) operator[](const K &key) const { + return table[projection(key) - offset]; + } + + /** + * @copydoc operator[](const K &key) + */ + decltype(auto) operator[](const K &key) { + return table[projection(key) - offset]; + } + + /** + * Whether a key is contained in the lookup table + * @param key key to check + * @return true if key is contained, false otherwise + */ + bool contains(const K &key) const noexcept { + auto pr = static_cast<long>(projection(key)); + return pr >= offset and pr <= maxKey(); + } + + /** + * Value access with bounds checking + * @param key Key + * @return stored value + * @throws std::out_of_range + */ + decltype(auto) at(const K &key) { + auto pr = static_cast<long>(projection(key)); + if (pr < offset or pr > maxKey()) { + throw std::out_of_range( + "Lookup::at out of range: key is " + std::to_string(pr) + ", offset is " + + std::to_string(offset) + ", max key is " + std::to_string(maxKey())); + } + + return this->operator[](key); + } + + /** + * @copydoc at(const K &key) + */ + decltype(auto) at(const K &key) const { + return std::as_const(traits::as_mut(*this).at(key)); + } + + /** + * Direct access to stored values + * @return + */ + auto &data() noexcept { + return table; + } + + /** + * @copydoc data() + */ + const auto &data() const noexcept { + return table; + } + + /** + * Number of entries + * @return number of entries + * @note also includes uninitialized values + */ + [[nodiscard]] std::size_t size() const noexcept { + return table.size(); + } + + /** + * Gets the lowest key stored + * @return projected value of lowest key stored + */ + [[nodiscard]] std::size_t keyOffset() const noexcept { + return offset; + } + + /** + * Gets the highest key stored + * @return projected value of highest key stored + */ + [[nodiscard]] long maxKey() const noexcept { + return static_cast<long>(table.size() + offset) - 1; + } + }; + + template<std::ranges::range Keys, typename T, concepts::ctyped_range<T> Values = std::vector<T>, typename P = + std::identity> + Lookup(const Keys &, const T & = {}, const Values & = {}, std::optional<std::pair<long, long>> = {}, + const P & = {}) -> Lookup<std::ranges::range_value_t<Keys>, T, P>; + +} + +#endif //LOOKUP_HPP diff --git a/src/header/util/printing.hpp b/src/header/util/printing.hpp new file mode 100644 index 0000000000000000000000000000000000000000..aec76a16704e3f48bdaedda036049b0c279f2c3f --- /dev/null +++ b/src/header/util/printing.hpp @@ -0,0 +1,33 @@ +/** +* @author Tim Luchterhand +* @date 09.01.25 +* @file printing.hpp +* @brief +*/ + +#ifndef PRINTING_HPP +#define PRINTING_HPP + +#include <ranges> +#include <ostream> + +template<std::ranges::range R> +auto printRange(const R &range, std::ostream &os) -> std::ostream& { + os << "["; + bool first = true; + for (const auto &elem : range) { + if (first) { + first = false; + } else { + os << ", "; + } + os << elem; + } + + os << "]"; + return os; +} + + + +#endif //PRINTING_HPP