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 &params,
                               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 &params, 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