diff --git a/Tests/data/extended_test_problem.json b/Tests/data/extended_test_problem.json
deleted file mode 100644
index ec324396698b02170e51101fd2f70f30b34bb375..0000000000000000000000000000000000000000
--- a/Tests/data/extended_test_problem.json
+++ /dev/null
@@ -1,57 +0,0 @@
-{
-  "constraints": [
-    [
-      4,
-      3,
-      0
-    ]
-  ],
-  "durations": [
-    5,
-    8,
-    7,
-    3
-  ],
-  "lowerBound": 0,
-  "optimalSolution": 10,
-  "resources": [
-    {
-      "capacity": 2,
-      "demand": [
-        2,
-        1
-      ],
-      "tasks": [
-        0,
-        2
-      ],
-      "transition": []
-    },
-    {
-      "capacity": 4,
-      "demand": [
-        3,
-        1,
-        1
-      ],
-      "tasks": [
-        1,
-        2,
-        3
-      ],
-      "transition": []
-    },
-    {
-      "capacity": 2,
-      "demand": [
-        2,
-        1
-      ],
-      "tasks": [
-        1,
-        3
-      ],
-      "transition": []
-    }
-  ]
-}
\ No newline at end of file
diff --git a/Tests/data/graph_builder_config.json b/Tests/data/graph_builder_config.json
index 03ad3f9e9d63bec4236bbbe4f9f269756f0c8032..a70efab2057ef7400c1512fb3e42b1d967801bc5 100644
--- a/Tests/data/graph_builder_config.json
+++ b/Tests/data/graph_builder_config.json
@@ -8,7 +8,9 @@
     "extractorName": "ResourceEnergyExtractor"
   },
   "taskFeatureExtractor": {
-    "arguments": null,
+    "arguments": {
+      "legacy": true
+    },
     "extractorName": "TaskTimingFeatureExtractor"
   },
   "topologyExtractor": {
diff --git a/Tests/data/test_problem.json b/Tests/data/test_problem.json
deleted file mode 100644
index 0a547ab8c40c4b42b2b701873e6900a601802541..0000000000000000000000000000000000000000
--- a/Tests/data/test_problem.json
+++ /dev/null
@@ -1,45 +0,0 @@
-{
-  "constraints": [
-    [
-      4,
-      3,
-      0
-    ]
-  ],
-  "durations": [
-    5,
-    8,
-    7,
-    3
-  ],
-  "lowerBound": 0,
-  "optimalSolution": 10,
-  "resources": [
-    {
-      "capacity": 2,
-      "demand": [
-        2,
-        1
-      ],
-      "tasks": [
-        0,
-        2
-      ],
-      "transition": []
-    },
-    {
-      "capacity": 4,
-      "demand": [
-        3,
-        1,
-        1
-      ],
-      "tasks": [
-        1,
-        2,
-        3
-      ],
-      "transition": []
-    }
-  ]
-}
\ No newline at end of file
diff --git a/Tests/heuristics/ValueHeuristics.cpp b/Tests/heuristics/ValueHeuristics.cpp
index 7da1916bd02f165dd5a9db51f3046a40f81f5d4c..95bbd9d6298ebaff9d375ba08d94de39ebf3bf37 100644
--- a/Tests/heuristics/ValueHeuristics.cpp
+++ b/Tests/heuristics/ValueHeuristics.cpp
@@ -62,8 +62,7 @@ struct LitProvider {
 
 TEST(value_heuristics, base_value_heuristic) {
     using namespace tempo;
-    using
-    enum tempo::VariableType;
+    using enum tempo::heuristics::VariableType;
     TestValueHeuristic h(0);
     auto lit = makeBooleanLiteral<int>(true, 0, 0);
     LitProvider provider(lit);
diff --git a/Tests/nn/GNN.cpp b/Tests/nn/GNN.cpp
index e4f2c922a1d4a01874449428590c3e2d1377fe54..67c0391bb66fdb015bf16a3ea0e3fde022b1e1c8 100644
--- a/Tests/nn/GNN.cpp
+++ b/Tests/nn/GNN.cpp
@@ -24,11 +24,6 @@ public:
     using tempo::nn::EdgeRegressor::extractHeatMap;
 };
 
-class TestGNNHeatMap: public tempo::nn::heuristics::GNNEdgePolarityPredictor {
-public:
-    using tempo::nn::heuristics::GNNEdgePolarityPredictor::choosePolarityFromHeatMap;
-};
-
 TEST(nn_GNN, base_gnn_ctor) {
     TestGNN gnn(tempo::testing::TestData::TestNN);
     EXPECT_FALSE(gnn.getModel().is_training());
@@ -54,18 +49,16 @@ TEST(nn_GNN, edge_regressor_heat_map){
 
 TEST(nn_GNN, gnn_heat_map_choose_polarity) {
     using namespace tempo;
+    using tempo::nn::heuristics::detail::choosePolarityFromHeatMap;
     Matrix<nn::DataType> heatMap(3, 3, nn::GNN::NoValue);
     heatMap(0, 1) = 0.9;
     heatMap(1, 0) = 0.1;
     heatMap(2, 1) = 1;
     heatMap(1, 2) = 1;
-    DistanceConstraint cp(START(0), END(1), 0);
-    EXPECT_TRUE(TestGNNHeatMap::choosePolarityFromHeatMap(cp.from, cp.to, heatMap));
-    EXPECT_FALSE(TestGNNHeatMap::choosePolarityFromHeatMap((~cp).from, (~cp).to, heatMap));
-    cp = {START(2), START(1), 0};
-    EXPECT_FALSE(TestGNNHeatMap::choosePolarityFromHeatMap(cp.from, cp.to, heatMap));
-    EXPECT_FALSE(TestGNNHeatMap::choosePolarityFromHeatMap((~cp).from, (~cp).to, heatMap));
-    cp = {START(0), END(2), 0};
-    EXPECT_THROW(TestGNNHeatMap::choosePolarityFromHeatMap(cp.from, cp.to, heatMap), std::runtime_error);
-    EXPECT_THROW(TestGNNHeatMap::choosePolarityFromHeatMap((~cp).from, (~cp).to, heatMap), std::runtime_error);
+    EXPECT_TRUE(choosePolarityFromHeatMap(0, 1, heatMap));
+    EXPECT_FALSE(choosePolarityFromHeatMap(1, 0, heatMap));
+    EXPECT_FALSE(choosePolarityFromHeatMap(2, 1, heatMap));
+    EXPECT_FALSE(choosePolarityFromHeatMap(1, 2, heatMap));
+    EXPECT_THROW(choosePolarityFromHeatMap(0, 2, heatMap), std::runtime_error);
+    EXPECT_THROW(choosePolarityFromHeatMap(2, 0, heatMap), std::runtime_error);
 }
diff --git a/Tests/nn/GraphBuilder.cpp b/Tests/nn/GraphBuilder.cpp
index 46b910ad2cfe9194e66e3c128df64764d529b697..b2ae274434e99f282e75003c5817e42e5e61c7b3 100644
--- a/Tests/nn/GraphBuilder.cpp
+++ b/Tests/nn/GraphBuilder.cpp
@@ -10,38 +10,34 @@
 #include "testing.hpp"
 
 
-auto getGtTopology(const ProblemInstance &problemInstance) -> tempo::nn::Topology {
+auto getGtTopology(const tempo::testing::ProblemInstance &problemInstance) -> tempo::nn::Topology {
     using namespace tempo::nn;
-    using namespace tempo::testing;
-    std::ifstream configFile(TestData::GraphBuilderConfig);
-    auto config = nlohmann::json::parse(configFile).get<GraphBuilderConfig>();
-    auto extractor = TopologyBuilderFactory::getInstance().create(config.topologyExtractor.extractorName,
-                                                                  problemInstance);
-    return std::visit([](auto &e) {
-        return e.getTopology(makeSolverState(tempo::Matrix<int>{}));
-    }, extractor);
+    MinimalTopologyBuilder builder(problemInstance);
+    return builder.getTopology();
 }
 
 void testFeatureExtraction(const std::string &featureName, const std::string &graphKey) {
     using namespace tempo::nn;
     using namespace tempo::testing;
-    const auto [problem, evtNetwork] = createRandomProblem(100, 50);
+    const auto [problem, scheduler, taskTimings] = createRandomProblem(100, 50);
     const auto topology = getGtTopology(problem);
     GraphBuilder graphBuilder(TestData::GraphBuilderConfig, problem);
-    auto graph = graphBuilder.getGraph(makeSolverState(evtNetwork));
+    const auto solverState = makeSolverState(taskTimings, scheduler);
+    auto graph = graphBuilder.getGraph(solverState);
     std::ifstream configFile(TestData::GraphBuilderConfig);
     auto config = nlohmann::json::parse(configFile).at(featureName).get<ExtractorConfig>();
     auto extractor = FeatureExtractorFactory::getInstance().create(config.extractorName, config.arguments);
-    auto gt = std::visit([&evtNetwork, &topology](auto &ext) { return ext(topology, evtNetwork); }, extractor);
+    auto gt = std::visit([&topology, &problem, &solverState](auto &ext) { return ext(topology, solverState, problem); },
+                         extractor);
     EXPECT_TRUE(torch::all(gt == graph.at(graphKey)).item<bool>());
 }
 
 TEST(nn_GraphBuilder, get_graph_basic) {
     using namespace tempo::nn;
     using namespace tempo::testing;
-    const auto [problem, evtNetwork] = createRandomProblem(3, 2);
+    const auto [problem, solver, taskNet] = createRandomProblem(3, 2);
     GraphBuilder graphBuilder(TestData::GraphBuilderConfig, problem);
-    auto graph = graphBuilder.getGraph(makeSolverState(evtNetwork));
+    auto graph = graphBuilder.getGraph(makeSolverState(taskNet, solver));
     EXPECT_TRUE(graph.contains(GraphKeys::TaskFeatures));
     EXPECT_TRUE(graph.contains(GraphKeys::EdgeFeatures));
     EXPECT_TRUE(graph.contains(GraphKeys::ResourceFeatures));
@@ -54,12 +50,10 @@ TEST(nn_GraphBuilder, get_graph_basic) {
 
 
 TEST(nn_GraphBuilder, get_graph_task_features) {
-    using namespace tempo::nn;
     testFeatureExtraction("taskFeatureExtractor", tempo::nn::GraphKeys::TaskFeatures);
 }
 
 TEST(nn_GraphBuilder, get_graph_edge_features) {
-    using namespace tempo::nn;
     testFeatureExtraction("edgeFeatureExtractor", tempo::nn::GraphKeys::EdgeFeatures);
 }
 
@@ -70,9 +64,9 @@ TEST(nn_GraphBuilder, get_graph_resource_features) {
 TEST(nn_GraphBuilder, get_graph_resource_deps) {
     using namespace tempo::nn;
     using namespace tempo::testing;
-    auto [problem, evtNetwork] = createRandomProblem(10, 5);
+    auto [problem, scheduler, taskNet] = createRandomProblem(10, 5);
     GraphBuilder graphBuilder(TestData::GraphBuilderConfig, problem);
-    auto graph = graphBuilder.getGraph(makeSolverState(evtNetwork));
+    auto graph = graphBuilder.getGraph(makeSolverState(taskNet, scheduler));
     const auto topology = getGtTopology(problem);
     EXPECT_TRUE(torch::all(topology.resourceDependencies == graph.at(GraphKeys::ResourceDependencies)).item<bool>());
     EXPECT_TRUE(torch::all(topology.resourceDemands == graph.at(GraphKeys::ResourceConsumptions)).item<bool>());
@@ -81,9 +75,9 @@ TEST(nn_GraphBuilder, get_graph_resource_deps) {
 TEST(nn_GraphBuilder, get_graph_topology) {
     using namespace tempo::nn;
     using namespace tempo::testing;
-    const auto [problem, evtNetwork] = createRandomProblem(10, 5);
+    const auto [problem, scheduler, taskNet] = createRandomProblem(10, 5);
     GraphBuilder graphBuilder(TestData::GraphBuilderConfig, problem);
-    const auto graph = graphBuilder.getGraph(makeSolverState(evtNetwork));
+    const auto graph = graphBuilder.getGraph(makeSolverState(taskNet, scheduler));
     const auto topology = getGtTopology(problem);
     EXPECT_TRUE(torch::all(topology.edgePairMask == graph.at(GraphKeys::EdgePairMask)).item<bool>());
     EXPECT_TRUE(torch::all(topology.edgeResourceRelations == graph.at(GraphKeys::EdgeResourceRelations)).item<bool>());
diff --git a/Tests/nn/MinimalTopologyBuilder.cpp b/Tests/nn/MinimalTopologyBuilder.cpp
index 94695fce29454d2ae094f43a5771b7a13cb12661..363b08c2d2c68b04040badde9013bc3ee9e3cd51 100644
--- a/Tests/nn/MinimalTopologyBuilder.cpp
+++ b/Tests/nn/MinimalTopologyBuilder.cpp
@@ -91,11 +91,15 @@ TEST(nn_MinimalTopologyBuilder, MinimalBuilder_addPrecedenceEdges) {
     using namespace tempo;
     using std::views::transform;
     tempo::nn::impl::TopologyData data{.edgeLookup = tempo::nn::impl::EdgeLookup(100)};
+    auto tasks = tempo::testing::createDummyTasks(5);
+    VarTaskMapping mapping(tasks);
     auto evtViewer = transform(
-            [](const auto &dc) { return DistanceConstraint<int>(START(dc.from), END(dc.to), dc.distance); });
+            [&tasks](const auto &dc) {
+                return DistanceConstraint<int>(tasks.at(dc.from).start.id(), tasks.at(dc.to).end.id(), dc.distance);
+            });
     std::vector<DistanceConstraint<int>> edges{{1, 2, 0}, {2, 3, 2}, {3, 4, -1}};
     TestMinimalTopologyBuilder::addEdge({2, 5}, true, -1, data);
-    TestMinimalTopologyBuilder::addPrecedenceEdges(edges | evtViewer, data);
+    TestMinimalTopologyBuilder::addPrecedenceEdges(edges | evtViewer, mapping, data);
     ASSERT_EQ(data.edges.size(), 4);
     EXPECT_EQ(data.edgeIdx.size(), 1);
     EXPECT_EQ(data.edgeIdx.front(), 0);
@@ -163,14 +167,16 @@ void testEdges(const EdgeVector &edges, const EdgeSet &gtEdges, const EdgeMap &e
     }
 }
 
-
 TEST(nn_MinimalTopologyBuilder, MinimalBuilder_completeSubGraphOneResource) {
-    std::vector<int> tasks{1, 2, 3};
+    using namespace tempo;
+    auto tasks = tempo::testing::createDummyTasks(4);
+    std::vector<unsigned> taskIds{1, 2, 3};
     std::vector<int> demands{2, 1, 4};
     constexpr int Capacity = 4;
-    Resource<int> resource(tasks, demands, {}, Capacity);
+    tempo::testing::Resource resource(Capacity, std::vector(tasks.begin() + 1, tasks.end()), demands);
+    VarTaskMapping mapping(tasks);
     tempo::nn::impl::TopologyData data{.edgeLookup = tempo::nn::impl::EdgeLookup(100)};
-    TestMinimalTopologyBuilder::completeSubGraph(resource, 17, data);
+    TestMinimalTopologyBuilder::completeSubGraph(resource, 17, mapping, data);
     ASSERT_EQ(data.taskIdx.size(), 3);
     ASSERT_EQ(data.resIdx.size(), 3);
     ASSERT_EQ(data.resDemands.size(), 3);
@@ -187,7 +193,7 @@ TEST(nn_MinimalTopologyBuilder, MinimalBuilder_completeSubGraphOneResource) {
         EXPECT_EQ(r, 17);
     }
 
-    for (auto [t, gtDemand] : iterators::const_zip(tasks, demands)) {
+    for (auto [t, gtDemand] : iterators::const_zip(taskIds, demands)) {
         auto res = std::ranges::find(data.taskIdx, t);
         ASSERT_NE(res, data.taskIdx.end());
         auto idx = static_cast<std::size_t>(res - data.taskIdx.begin());
@@ -222,15 +228,22 @@ void checkEdge(const Edge &edge, const EdgeVector &allEdges, std::vector<IndexTy
 }
 
 TEST(nn_MinimalTopologyBuilder, MinimalBuilder_completeSubGraph_multiple_resources) {
+    using namespace tempo;
+    auto tasks = tempo::testing::createDummyTasks(4);
+    VarTaskMapping mapping(tasks);
     std::vector<int> tasks17{1, 2};
     std::vector<int> demands17{2, 1};
     constexpr int Capacity17 = 2;
     tempo::nn::impl::TopologyData data{.edgeLookup = tempo::nn::impl::EdgeLookup(100)};
-    TestMinimalTopologyBuilder::completeSubGraph(Resource<int>(tasks17, demands17, {}, Capacity17), 17, data);
+    TestMinimalTopologyBuilder::completeSubGraph(
+            tempo::testing::Resource(Capacity17, std::vector(tasks.begin() + 1, tasks.begin() + 3), demands17),
+            17, mapping, data);
     decltype(tasks17) tasks18{1, 2, 3};
     decltype(demands17) demands18{2, 1, 1};
     constexpr int Capacity18 = 3;
-    TestMinimalTopologyBuilder::completeSubGraph(Resource<int>(tasks18, demands18, {}, Capacity18), 18, data);
+    TestMinimalTopologyBuilder::completeSubGraph(
+            tempo::testing::Resource(Capacity18, std::vector(tasks.begin() + 1, tasks.end()), demands18),
+            18, mapping, data);
     ASSERT_EQ(data.taskIdx.size(), 5);
     ASSERT_EQ(data.resIdx.size(), 5);
     ASSERT_EQ(data.resDemands.size(), 5);
diff --git a/Tests/nn/TopologyBuilderTest.cpp b/Tests/nn/TopologyBuilderTest.cpp
index 4c5a146197ec301f79aa14b0ee87ea73fd129880..b0d35bb1fe31ea7dc45983a93ed7fdf36d73e790 100644
--- a/Tests/nn/TopologyBuilderTest.cpp
+++ b/Tests/nn/TopologyBuilderTest.cpp
@@ -13,15 +13,16 @@
 #include "Tests/testing.hpp"
 
 void TopologyBuilderTest::SetUp() {
-    inst = tempo::serialization::deserializeFromFile<ProblemInstance>(tempo::testing::TestData::TestProblem);
-    extInst = tempo::serialization::deserializeFromFile<ProblemInstance>(tempo::testing::TestData::ExtendedTestProblem);
+    auto [i, _] = tempo::testing::createTestProblem();
+    inst = std::move(i);
+    std::tie(extInst, _) = tempo::testing::createExtendedTestProblem();
 }
 
-auto TopologyBuilderTest::instance() const noexcept -> const ProblemInstance & {
+auto TopologyBuilderTest::instance() const noexcept -> const tempo::testing::ProblemInstance & {
     return inst;
 }
 
-auto TopologyBuilderTest::extendedInstance() const noexcept -> const ProblemInstance & {
+auto TopologyBuilderTest::extendedInstance() const noexcept -> const tempo::testing::ProblemInstance & {
     return extInst;
 }
 
@@ -65,11 +66,12 @@ void TopologyBuilderTest::testResourceDependencies(const tempo::nn::Topology &to
                                        std::span(topology.resourceDemands.data_ptr<DataType>(), tasks.size(0)));
     for (auto [tIdx, rIdx, d] : zRange) {
         EXPECT_GT(resMat.at(rIdx, tIdx), 0);
-        EXPECT_EQ(static_cast<DataType>(resMat.at(rIdx, tIdx)) / instance().resources[rIdx].capacity, d);
+        EXPECT_EQ(static_cast<DataType>(resMat.at(rIdx, tIdx)) / instance().resources()[rIdx].resourceCapacity(), d);
     }
 
-    using namespace std::views;
-    for (auto t = 0ul; t < instance().durations.size(); ++t) {
+    using std::views::iota;
+    using std::views::filter;
+    for (auto t = 0ul; t < instance().tasks().size(); ++t) {
         auto gtRange =
                 iota(0ul, resMat.numRows()) | filter([&resMat, t](auto r) { return resMat.at(r, t) > 0; });
         std::unordered_set<std::size_t> gtResources(gtRange.begin(), gtRange.end());
@@ -83,15 +85,14 @@ void TopologyBuilderTest::testResourceDependencies(const tempo::nn::Topology &to
 
 void TopologyBuilderTest::testEdges(const tempo::nn::Topology &topology) const {
     using namespace tempo::nn;
-    using tempo::TASK;
     const auto consumptions = getResourceMatrix(instance());
     auto edges = util::getEdgeView(topology.edgeIndices);
     EXPECT_EQ(edges.size(), 9); // only for this particular test problem
     for (auto [from, to] : edges) {
-        auto res = std::ranges::find_if(instance().constraints, [from, to](const auto &prec) {
-            return TASK(std::get<0>(prec)) == from and TASK(std::get<1>(prec)) == to;
+        auto res = std::ranges::find_if(instance().precedences(), [from, to, this](const auto &prec) {
+            return instance().getMapping()(prec.from) == from and instance().getMapping()(prec.to) == to;
         });
-        if (res != instance().constraints.end()) {
+        if (res != instance().precedences().end()) {
             continue;
         }
 
@@ -144,11 +145,11 @@ void TopologyBuilderTest::testEdgePairMask(const tempo::nn::Topology &topology)
     }
 }
 
-auto TopologyBuilderTest::getResourceMatrix(const ProblemInstance &probInstance) -> tempo::Matrix<int> {
-    tempo::Matrix<int> ret(probInstance.resources.size(), probInstance.durations.size());
-    for (const auto [r, res] : iterators::const_enumerate(probInstance.resources)) {
+auto TopologyBuilderTest::getResourceMatrix(const tempo::testing::ProblemInstance &probInstance) -> tempo::Matrix<int> {
+    tempo::Matrix<int> ret(probInstance.resources().size(), probInstance.tasks().size());
+    for (const auto [r, res] : iterators::const_enumerate(probInstance.resources())) {
         for (auto [idx, t] : iterators::enumerate(res)) {
-            ret.at(r, t) = res.getDemand(idx);
+            ret.at(r, probInstance.getMapping()(t.id())) = res.getDemand(idx);
         }
     }
 
diff --git a/Tests/nn/TopologyBuilderTest.hpp b/Tests/nn/TopologyBuilderTest.hpp
index 07a0f5888f1dd614bfba3e05e180a84ec735c2e3..4234a20f24c40de07873c3c1d92b8871196c2186 100644
--- a/Tests/nn/TopologyBuilderTest.hpp
+++ b/Tests/nn/TopologyBuilderTest.hpp
@@ -8,20 +8,22 @@
 
 #include <gtest/gtest.h>
 
-#include "util/parsing/format.hpp"
+#include "testing.hpp"
 #include "nn/torch_types.hpp"
 #include "util/Matrix.hpp"
+#include "Model.hpp"
+#include "util/SchedulingProblemHelper.hpp"
 
 class TopologyBuilderTest : public testing::Test {
-    ProblemInstance inst;
-    ProblemInstance extInst;
+    tempo::testing::ProblemInstance inst;
+    tempo::testing::ProblemInstance extInst;
 
 protected:
     void SetUp() override;
 
-    [[nodiscard]] auto instance() const noexcept -> const ProblemInstance&;
+    [[nodiscard]] auto instance() const noexcept -> const tempo::testing::ProblemInstance&;
 
-    [[nodiscard]] auto extendedInstance() const noexcept -> const ProblemInstance&;
+    [[nodiscard]] auto extendedInstance() const noexcept -> const tempo::testing::ProblemInstance&;
 
     void testResourceDependencies(const tempo::nn::Topology &topology) const;
 
@@ -29,7 +31,7 @@ protected:
 
     void testEdgePairMask(const tempo::nn::Topology &topology) const;
 
-    [[nodiscard]] static auto getResourceMatrix(const ProblemInstance &probInstance) -> tempo::Matrix<int>;
+    [[nodiscard]] static auto getResourceMatrix(const tempo::testing::ProblemInstance &probInstance) -> tempo::Matrix<int>;
 
 public:
     static void testEdgeResourceRelations(const torch::Tensor &edgeResourceRelations,
diff --git a/Tests/nn/feature_extractors.cpp b/Tests/nn/feature_extractors.cpp
index 8f4caf91c2421fec753b4cd352261b5ecd68ce78..2861df7ce35006a3836e0416c0b9e9e5948df022 100644
--- a/Tests/nn/feature_extractors.cpp
+++ b/Tests/nn/feature_extractors.cpp
@@ -6,15 +6,16 @@
 #include <gtest/gtest.h>
 #include <Iterators.hpp>
 
-#include "util/parsing/format.hpp"
 #include "testing.hpp"
 #include "nn/feature_extractors.hpp"
 #include "util/Matrix.hpp"
 #include "nn/GraphBuilder.hpp"
+#include "util/SchedulingProblemHelper.hpp"
 
-auto getInput() -> std::pair<ProblemInstance, tempo::nn::Topology> {
+
+auto getInput() -> std::pair<tempo::testing::ProblemInstance, tempo::nn::Topology> {
     using namespace tempo;
-    auto problem = serialization::deserializeFromFile<ProblemInstance>(tempo::testing::TestData::TestProblem);
+    auto [problem, _] = tempo::testing::createTestProblem();
     nn::MinimalTopologyBuilder topologyBuilder(problem);
     auto topology = topologyBuilder.getTopology();
     return {std::move(problem), std::move(topology)};
@@ -23,52 +24,70 @@ auto getInput() -> std::pair<ProblemInstance, tempo::nn::Topology> {
 TEST(nn_feature_extractors, TaskTimingExtractor) {
     using namespace tempo::nn;
     using namespace tempo;
+    using namespace tempo::testing;
+    using T = tempo::testing::TaskSpec;
     using tempo::testing::random_int;
-    using tempo::testing::setTaskDurations;
-    using tempo::testing::setUpperBound;
-    constexpr bool SatisfiesConcept = feature_extractor<TaskTimingFeatureExtractor, Matrix<int>>;
     constexpr DataType Ub = 10;
-    EXPECT_TRUE(SatisfiesConcept);
     TaskTimingFeatureExtractor extractor;
-    const auto [instance, topology] = getInput();
-    const auto numEvents = instance.durations.size() * 2 + 2;
-    Matrix<int> eventNet(numEvents, numEvents);
-    std::vector<tempo::testing::TaskSpec> taskSpecs;
-    for (std::size_t t = 0; t < instance.durations.size(); ++t) {
-        taskSpecs.emplace_back(random_int(1, 5), random_int(5, 10), random_int(-10, 0), random_int(-10, 0));
-        setTaskDurations(static_cast<tempo::task>(t), taskSpecs.back().minDur, taskSpecs.back().maxDur,
-                         taskSpecs.back().release, taskSpecs.back().deadline, eventNet);
-    }
-
-    setUpperBound<int>(Ub, eventNet);
-    auto taskFeatures = extractor(topology, eventNet);
-    for (auto [idx, task] : iterators::const_enumerate(taskSpecs)) {
+    auto [tasks, scheduler] = createTasks(
+            {T{.minDur = 2, .maxDur = 4, .earliestStart = 4, .latestDeadline = static_cast<int>(Ub)},
+             T{.minDur = 6, .maxDur = 6, .earliestStart = 0, .latestDeadline = 5},
+             T{.minDur = 4, .maxDur = 6, .earliestStart = 2, .latestDeadline = 8},
+             T{.minDur = 0, .maxDur = static_cast<int>(Ub), .latestDeadline = static_cast<int>(Ub)}});
+    auto sched = tasks.back();
+    tasks.pop_back();
+    ProblemInstance instance(std::move(tasks), {}, {}, sched);
+    auto topology = MinimalTopologyBuilder(instance).getTopology();
+    auto taskFeatures = extractor(topology, makeSolverState(Matrix<int>{}, scheduler), instance);
+    for (auto [idx, task] : iterators::const_enumerate(instance.tasks())) {
         auto features = taskFeatures.slice(0, idx, idx + 1);
         EXPECT_EQ(features.numel(), 4);
-        EXPECT_EQ(features[0][0].item<DataType>(), task.minDur/ Ub);
-        EXPECT_EQ(features[0][1].item<DataType>(), task.maxDur / Ub);
-        EXPECT_EQ(features[0][2].item<DataType>(), task.release / Ub);
-        EXPECT_EQ(features[0][3].item<DataType>(), task.deadline / Ub);
+        EXPECT_EQ(features[0][0].item<DataType>(), task.minDuration(scheduler) / Ub);
+        EXPECT_EQ(features[0][1].item<DataType>(), task.maxDuration(scheduler) / Ub);
+        EXPECT_EQ(features[0][2].item<DataType>(), task.getEarliestStart(scheduler) / Ub);
+        EXPECT_EQ(features[0][3].item<DataType>(), task.getLatestEnd(scheduler) / Ub);
     }
 }
 
+TEST(nn_feature_extractors, TaskTimingExtractor_legacy) {
+    using namespace tempo;
+    using namespace tempo::nn;
+    using namespace tempo::testing;
+    using T = tempo::testing::TaskSpec;
+    TaskTimingFeatureExtractor extractor{.legacyFeatures = false};
+    constexpr int Ub = 10;
+    auto [tasks, scheduler] = createTasks({T{.minDur = 4, .maxDur = 6, .earliestStart = 2, .latestDeadline = Ub - 2},
+                                           T{.minDur = 0, .maxDur = Ub, .latestDeadline = Ub}});
+    auto sched = tasks.back();
+    tasks.pop_back();
+    ProblemInstance instance(std::move(tasks), {}, {}, sched);
+    auto topology = MinimalTopologyBuilder(instance).getTopology();
+    auto taskFeatures = extractor(topology, makeSolverState(Matrix<int>{}, scheduler), instance);
+    ASSERT_EQ(taskFeatures.numel(), 4);
+    EXPECT_FLOAT_EQ(taskFeatures[0][0].item<DataType>(), 0.4);
+    EXPECT_FLOAT_EQ(taskFeatures[0][1].item<DataType>(), 0.6);
+    EXPECT_FLOAT_EQ(taskFeatures[0][2].item<DataType>(), 0.2);
+    EXPECT_FLOAT_EQ(taskFeatures[0][3].item<DataType>(), 0.8);
+    extractor.legacyFeatures = true;
+    taskFeatures = extractor(topology, makeSolverState(Matrix<int>{}, scheduler), instance);
+    EXPECT_FLOAT_EQ(taskFeatures[0][0].item<DataType>(), 0.4);
+    EXPECT_FLOAT_EQ(taskFeatures[0][1].item<DataType>(), 0.6);
+    EXPECT_FLOAT_EQ(taskFeatures[0][2].item<DataType>(), -0.2);
+    EXPECT_FLOAT_EQ(taskFeatures[0][3].item<DataType>(), -0.2);
+}
+
 TEST(nn_feature_extractors, ResourceEnergyExtractor) {
     using namespace tempo::nn;
     using namespace tempo::serialization;
     using namespace tempo;
     using namespace tempo::testing;
-    constexpr bool SatisfiesConcept = feature_extractor<ResourceEnergyExtractor, Matrix<int>>;
-    EXPECT_TRUE(SatisfiesConcept);
-    ProblemInstance instance{.lowerBound = 0, .optimalSolution = 0, .durations = {1, 1, 1},
-                             .constraints = {}, .resources = {{{0, 1, 2}, {1, 1, 1}, {}, 1}}};
+    auto [tasks, scheduler] = tempo::testing::createTasks({{6, 6}, {2, 4}, {1, 5}, {0, 12, 0, 12}});
+    auto sched = tasks.back();
+    tasks.pop_back();
+    ProblemInstance instance(tasks, {{1, tasks, {1, 1, 1}}}, {}, sched);
     const auto topology = MinimalTopologyBuilder(instance).getTopology();
-    Matrix<int> eventNet(8, 8);
-    setTaskDurations(0, 6, 6, 0, 0, eventNet);
-    setTaskDurations(1, 2, 4, 0, 0, eventNet);
-    setTaskDurations(2, 1, 5, 0, 0, eventNet);
-    setUpperBound(12, eventNet);
     ResourceEnergyExtractor extractor;
-    auto resEnergies = extractor(topology, eventNet);
+    auto resEnergies = extractor(topology, makeSolverState(Matrix<int>{}, scheduler), instance);
     ASSERT_EQ(resEnergies.size(0), 1);
     ASSERT_EQ(resEnergies.size(1), 1);
     EXPECT_EQ(resEnergies.item<DataType>(), 1);
@@ -79,16 +98,13 @@ TEST(nn_features_extractors, ResourceEnergyExtractor_complex_consumptions) {
     using namespace tempo::serialization;
     using namespace tempo;
     using namespace tempo::testing;
-    ProblemInstance instance{.lowerBound = 0, .optimalSolution = 0, .durations = {1, 1, 1},
-            .constraints = {}, .resources = {{{0, 1, 2}, {3, 2, 2}, {}, 4}}};
+    auto [tasks, scheduler] = tempo::testing::createTasks({{4, 4}, {2, 4}, {8, 10}, {0, 9, 0, 9}});
+    auto sched = tasks.back();
+    tasks.pop_back();
+    ProblemInstance instance(tasks, {{4, tasks, {3, 2, 2}}}, {}, sched);
     const auto topology = MinimalTopologyBuilder(instance).getTopology();
-    Matrix<int> eventNet(8, 8);
-    setTaskDurations(0, 4, 4, 0, 0, eventNet);
-    setTaskDurations(1, 2, 4, 0, 0, eventNet);
-    setTaskDurations(2, 8, 10, 0, 0, eventNet);
-    setUpperBound(9, eventNet);
     ResourceEnergyExtractor extractor;
-    auto resEnergies = extractor(topology, eventNet);
+    auto resEnergies = extractor(topology, makeSolverState(Matrix<int>{}, scheduler), instance);
     ASSERT_EQ(resEnergies.size(0), 1);
     ASSERT_EQ(resEnergies.size(1), 1);
     EXPECT_EQ(resEnergies.item<DataType>(), 1);
@@ -98,19 +114,15 @@ TEST(nn_feature_extractors, TimingEdgeExtractor) {
     using namespace tempo::nn;
     using namespace tempo::testing;
     using namespace tempo;
-    constexpr bool SatisfiesConcept = feature_extractor<TimingEdgeExtractor, Matrix<int>>;
-    EXPECT_TRUE(SatisfiesConcept);
-    tempo::Matrix<int> timings(8, 8);
-    setTaskDistance(0, 1, 4, timings);
-    setTaskDistance(1, 0, 3, timings);
-    setTaskDistance(2, 1, -2, timings);
-    setTaskDistance(1, 2, 5, timings);
-    setUpperBound(4, timings);
-    timings.at(1, 2) = 5;
+    tempo::Matrix<int> timings(3, 3, {0, 4, 0,
+                                      3, 0, 5,
+                                      0, -2, 0});
+    auto [schedule, scheduler] = createTasks({{0, 4, 0, 4}});
     Topology topology;
     EdgeVector edges{{0, 1}, {2, 1}};
     topology.edgeIndices = util::makeIndexTensor(edges);
-    torch::Tensor features = TimingEdgeExtractor()(topology, timings);
+    torch::Tensor features = TimingEdgeExtractor()(topology, makeSolverState(timings, scheduler),
+                                                   ProblemInstance({}, {}, {}, schedule.front()));
     ASSERT_EQ(features.size(0), edges.size());
     ASSERT_EQ(features.size(1), 2);
     ASSERT_EQ(features.sizes().size(), 2);
diff --git a/Tests/testing.cpp b/Tests/testing.cpp
index 4bcd7dc2793069bbf48f8bf0e71125837b105269..3d84950f52dc1e79a6b3df0479c08dab0f7dcc28 100644
--- a/Tests/testing.cpp
+++ b/Tests/testing.cpp
@@ -8,5 +8,134 @@
 
 namespace tempo::testing {
 
+    auto createTestProblem() -> std::pair<ProblemInstance, DummyScheduler> {
+        using namespace tempo;
+        auto [tasks, scheduler] = createTasks({TaskSpec{.minDur = 2, .maxDur = 5},
+                                               TaskSpec{.minDur = 6, .maxDur = 6},
+                                               TaskSpec{.minDur = 2, .maxDur = 7},
+                                               TaskSpec{.minDur = 2, .maxDur = 3},
+                                               TaskSpec{.minDur = 0, .maxDur = Constant::Infinity<int>}});
+        auto schedule = tasks.back();
+        tasks.pop_back();
+        std::vector<Resource> resources{{2, {tasks[0], tasks[2]},           {2, 1}},
+                                        {3, {tasks[1], tasks[2], tasks[3]}, {3, 1, 1}}};
+        std::vector<DistanceConstraint<int>> precedences{{tasks[1].start.id(), tasks[0].end.id(), 0}};
+        return {ProblemInstance(std::move(tasks), std::move(resources), std::move(precedences), schedule),
+                std::move(scheduler)} ;
+    }
+
+    auto createExtendedTestProblem() -> std::pair<ProblemInstance, DummyScheduler> {
+        auto [problem, scheduler] = createTestProblem();
+        auto &resources = const_cast<std::remove_cvref_t<decltype(problem.resources())>&>(problem.resources());
+        resources.emplace_back(2, std::vector{problem.tasks().at(1), problem.tasks().at(3)}, std::vector{2, 1});
+        return {std::move(problem), std::move(scheduler)};
+    }
+
+
+    auto createTasks(const std::vector<TaskSpec> &specs) -> std::pair<std::vector<Interval<int>>, DummyScheduler> {
+        std::vector<Interval<int>> ret;
+        std::vector<int> upper(specs.size() * VarTaskMapping::NumTemporalVarPerTask, Constant::Infinity<int>);
+        std::vector<int> lower(specs.size() * VarTaskMapping::NumTemporalVarPerTask, 0);
+        unsigned idx = 0;
+        for (const auto &spec : specs) {
+            if (spec.minDur == spec.maxDur) {
+                ret.emplace_back(NumericVar{idx, 0}, NumericVar{idx, spec.minDur}, NumericVar{idx + 1, 0});
+            } else {
+                ret.emplace_back(NumericVar{idx, 0}, NumericVar{idx + 1, 0}, NumericVar{idx + 2, 0});
+                ++idx;
+            }
+
+            lower.at(ret.back().duration.id()) = spec.minDur;
+            upper.at(ret.back().duration.id()) = spec.maxDur;
+            lower.at(ret.back().start.id()) = spec.earliestStart;
+            upper.at(ret.back().end.id()) = spec.latestDeadline;
+            idx += 2;
+        }
+
+        return {std::move(ret), DummyScheduler(std::move(upper), std::move(lower))};
+    }
+
+    auto createDummyTasks(unsigned int numberOfTasks) -> std::vector<Interval<int>> {
+        std::vector<Interval<int>> ret;
+        ret.reserve(numberOfTasks);
+        for (unsigned i = 0; i < numberOfTasks; ++i) {
+            ret.emplace_back(NumericVar{i, 0}, NumericVar{i, 0}, NumericVar{i, 0});
+        }
+
+        return ret;
+    }
+
+    auto createRandomProblem(std::size_t numTasks, std::size_t numResources,
+                             double precedenceChance) -> std::tuple<ProblemInstance, DummyScheduler, Matrix<int>> {
+        assert(precedenceChance >= 0 and precedenceChance <= 1);
+        using namespace tempo;
+        std::vector<DistanceConstraint<int>> precedences;
+        Matrix<int> eventNetwork(2 * numTasks + 2, 2 * numTasks + 2);
+        int upperBound = 0;
+        std::vector<TaskSpec> taskSpecs;
+        taskSpecs.reserve(numTasks + 1);
+        for (int t = 0; t < static_cast<int>(numTasks); ++t) {
+            taskSpecs.emplace_back(random_int(2, 6), random_int(7, 10), random_int(-7, 0), random_int(-7, 0));
+            upperBound += taskSpecs.back().maxDur;
+        }
+
+        taskSpecs.emplace_back(upperBound / 2, upperBound, 0, upperBound);
+        auto [tasks, scheduler] = createTasks(taskSpecs);
+        auto sched = tasks.back();
+        tasks.pop_back();
+        Matrix<int> taskDistances(numTasks, numTasks, upperBound);
+        for (unsigned from = 0; from < numTasks; ++from) {
+            for(unsigned to = 0; to < numTasks; ++to) {
+                if (from == to) {
+                    continue;
+                }
+
+                taskDistances.at(from, to) = random_int(-20 * static_cast<int>(precedenceChance),
+                                                        20 * static_cast<int>(1 - precedenceChance));
+                if (taskDistances.at(from, to) <= 0) {
+                    precedences.emplace_back(tasks.at(from).start.id(), tasks.at(to).end.id(),
+                                             eventNetwork.at(from, to));
+                }
+            }
+        }
+
+        std::vector<Resource> resources;
+        resources.reserve(numResources);
+        for (auto r = 0ul; r < numResources; ++r) {
+            decltype(tasks) consuming;
+            std::vector<int> demands;
+            const auto capacity = random_int(1, 5);
+            for (auto t = 0ul; t < numTasks; ++t) {
+                auto demand = random_int(0, capacity);
+                if (demand > 0) {
+                    consuming.emplace_back(tasks.at(t));
+                    demands.emplace_back(demand);
+                }
+            }
+
+            resources.emplace_back(capacity, std::move(consuming), std::move(demands));
+        }
+
+        return {ProblemInstance(std::move(tasks), std::move(resources), std::move(precedences), sched),
+                std::move(scheduler), std::move(taskDistances)};
+
+    }
+
+    Resource::Resource(int capacity, std::vector<Interval<int>> tasks, std::vector<int> demands)
+            : std::vector<Interval<int>>(std::move(tasks)), demands(std::move(demands)), capacity(capacity) {}
+
+    int Resource::getDemand(unsigned int taskId) const {
+        return demands.at(taskId);
+    }
+
+    int Resource::resourceCapacity() const {
+        return capacity;
+    }
+
+    BoundProvider::BoundProvider(std::vector<int> upper, std::vector<int> lower) : u(std::move(upper)), l(std::move(lower)) {}
+
+    int BoundProvider::upper(tempo::var_t var) const { return u.at(var); }
+
+    int BoundProvider::lower(tempo::var_t var) const { return l.at(var); }
 }
 
diff --git a/Tests/testing.hpp b/Tests/testing.hpp
index 7b00f2035282e309e7810c9f1f8533cc1cc82f40..d6b4ff8911d6636e0e745a7c47809a83c8543387 100644
--- a/Tests/testing.hpp
+++ b/Tests/testing.hpp
@@ -3,25 +3,66 @@
  * @date 27.04.23.
  */
 
-#ifndef SCHEDCL_TESTING_HPP
-#define SCHEDCL_TESTING_HPP
+#ifndef TEMPO_TESTING_HPP
+#define TEMPO_TESTING_HPP
 
 #include <filesystem>
 #include <random>
 #include <concepts>
+#include <utility>
+#include <tuple>
 
 #include "util/Matrix.hpp"
 #include "util/serialization.hpp"
 #include "Global.hpp"
+#include "util/SchedulingProblemHelper.hpp"
+#include "Model.hpp"
 
 namespace tempo::testing {
     struct TestData {
-        static constexpr auto TestProblem = __TEST_DATA_DIR__ "/test_problem.json";
-        static constexpr auto ExtendedTestProblem = __TEST_DATA_DIR__ "/extended_test_problem.json";
         static constexpr auto GraphBuilderConfig = __TEST_DATA_DIR__ "/graph_builder_config.json";
         static constexpr auto TestNN = __TEST_DATA_DIR__ "/Identity_export.pt";
     };
 
+    struct Resource : public std::vector<Interval<int>> {
+        std::vector<int> demands;
+        int capacity;
+        Resource(int capacity, std::vector<Interval<int>> tasks, std::vector<int> demands);
+        [[nodiscard]] int getDemand(unsigned taskId) const;
+        [[nodiscard]] int resourceCapacity() const;
+    };
+
+    using ProblemInstance = tempo::SchedulingProblemHelper<int, Resource>;
+
+    class BoundProvider {
+        std::vector<int> u, l;
+    public:
+        BoundProvider(std::vector<int> upper, std::vector<int> lower);
+        [[nodiscard]] int upper(tempo::var_t var) const;
+        [[nodiscard]] int lower(tempo::var_t var) const;
+    };
+
+    struct DummyScheduler {
+        tempo::testing::BoundProvider numeric;
+        template<typename ...Args>
+        explicit DummyScheduler(Args &&...args): numeric(std::forward<Args>(args)...) {}
+    };
+
+
+    auto createTestProblem() -> std::pair<ProblemInstance, DummyScheduler>;
+
+    auto createExtendedTestProblem() -> std::pair<ProblemInstance, DummyScheduler>;
+
+    auto createRandomProblem(std::size_t numTasks, std::size_t numResources,
+                             double precedenceChance = 0.3) -> std::tuple<ProblemInstance, DummyScheduler, Matrix<int>>;
+
+    struct TaskSpec {
+        int minDur, maxDur, earliestStart{0}, latestDeadline{0};
+    };
+
+    auto createTasks(const std::vector<TaskSpec> &specs) -> std::pair<std::vector<Interval<int>>, DummyScheduler>;
+
+    auto createDummyTasks(unsigned numberOfTasks) -> std::vector<Interval<int>>;
 
     /**
      * Generates a random integer value in [min, max]
@@ -52,11 +93,6 @@ namespace tempo::testing {
         std::uniform_real_distribution<T> dist(min, max);
         return dist(el);
     }
-
-    struct TaskSpec {
-        int minDur, maxDur, release, deadline;
-    };
-
 }
 
-#endif //SCHEDCL_TESTING_HPP
+#endif //TEMPO_TESTING_HPP
diff --git a/Tests/util/scheduling_problem_utils.cpp b/Tests/util/scheduling_problem_utils.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..df4b6b8b74ed9985b1c73271dea7767963cc52cd
--- /dev/null
+++ b/Tests/util/scheduling_problem_utils.cpp
@@ -0,0 +1,95 @@
+/**
+* @author Tim Luchterhand
+* @date 01.07.24
+* @brief
+*/
+
+#include <gtest/gtest.h>
+
+#include "util/SchedulingProblemHelper.hpp"
+#include "Model.hpp"
+#include "testing.hpp"
+
+TEST(util, VarTaskMapping) {
+    using namespace tempo;
+    std::vector<Interval<>> tasks(3);
+    tasks[0].start = {4, 0};
+    tasks[0].end = {4, 7};
+    tasks[1].start = {6, 0};
+    tasks[1].end = {7, 0};
+    tasks[2].start = {5, 0};
+    tasks[2].end = {5, 2};
+    VarTaskMapping mapping(tasks);
+    EXPECT_TRUE(mapping.contains(6));
+    EXPECT_TRUE(mapping.contains(4));
+    EXPECT_FALSE(mapping.contains(-2));
+    EXPECT_FALSE(mapping.contains(8));
+    EXPECT_EQ(mapping(5), 2);
+    EXPECT_EQ(mapping(7), mapping(6));
+    EXPECT_EQ(mapping(4), 0);
+    std::vector<Interval<>> empty;
+    VarTaskMapping emptyMapping(empty);
+    EXPECT_FALSE(emptyMapping.contains(0));
+    EXPECT_FALSE(emptyMapping.contains(3));
+}
+
+TEST(util, VarTaskMapping_non_continuous) {
+    using namespace tempo;
+    std::vector<Interval<>> tasks(3);
+    tasks[0].start = {4, 0};
+    tasks[0].end = {5, 0};
+    tasks[1].start = {18, 0};
+    tasks[1].end = {19, 0};
+    tasks[2].start = {9, 0};
+    tasks[2].end = {10, 2};
+    EXPECT_THROW(VarTaskMapping{tasks}, std::runtime_error);
+}
+
+TEST(util, SchedulingProblemHelper_basic) {
+    using namespace tempo;
+    using T = tempo::testing::TaskSpec;
+    auto [tasks, _] = tempo::testing::createTasks(
+            {T{.minDur = 3, .maxDur = 6}, T{.minDur = 5, .maxDur = 5}, T{.minDur = 1, .maxDur = 7},
+             T{.minDur = 0, .maxDur = 100}});
+    auto sched = tasks.back();
+    tasks.pop_back();
+    SchedulingProblemHelper<int, tempo::testing::Resource> schedulingProb(tasks, {}, {}, sched);
+    EXPECT_EQ(tasks, schedulingProb.tasks());
+    EXPECT_TRUE(schedulingProb.hasTask(0));
+    EXPECT_TRUE(schedulingProb.hasTask(1));
+    EXPECT_TRUE(schedulingProb.hasTask(2));
+    EXPECT_FALSE(schedulingProb.hasTask(3));
+
+    EXPECT_TRUE(schedulingProb.hasVariable(tasks[0].start.id()));
+    EXPECT_TRUE(schedulingProb.hasVariable(tasks[0].end.id()));
+    EXPECT_TRUE(schedulingProb.hasVariable(tasks[1].start.id()));
+    EXPECT_TRUE(schedulingProb.hasVariable(tasks[1].end.id()));
+    EXPECT_TRUE(schedulingProb.hasVariable(tasks[2].start.id()));
+    EXPECT_TRUE(schedulingProb.hasVariable(tasks[2].end.id()));
+    EXPECT_FALSE(schedulingProb.hasVariable(tasks[1].duration.id()));
+    EXPECT_FALSE(schedulingProb.hasVariable(120));
+
+    EXPECT_EQ(schedulingProb.getTask(tasks.back().end.id()), tasks.back());
+    EXPECT_EQ(schedulingProb.getTask(tasks.front().id()), tasks.front());
+}
+
+TEST(util, SchedulingProblemView_task_distances) {
+    using namespace tempo;
+    std::vector<Interval<int>> tasks{{{0, 0}, {1, 0}, NumericVar{}},
+                                     {{2, 0}, {2, 5}, NumericVar{}},
+                                     {{3, 0}, {4, 0}, NumericVar{}}};
+    SchedulingProblemHelper<int, tempo::testing::Resource> schedulingProb(tasks, {}, {}, Interval<int>{});
+    BacktrackEnvironment env;
+    DirectedGraph<LabeledEdge<int>> graph(5, &env);
+    graph.emplace_edge(2, 4, -2);
+    graph.emplace_edge(0, 2, -1);
+    graph.emplace_edge(0, 3, 4);
+    tempo::testing::BoundProvider bounds({6, 5, 3, 8, 4}, {3, 2, 0, 4, 1});
+    auto distance = schedulingProb.getTaskDistances(graph, bounds);
+    EXPECT_EQ(distance(0, 1), 4);
+    EXPECT_EQ(distance(1, 0), 1);
+    EXPECT_EQ(distance(0, 2), 5);
+    EXPECT_EQ(distance(2, 0), 6);
+    EXPECT_EQ(distance(2, 1), 13);
+    EXPECT_EQ(distance(1, 2), -2);
+}
\ No newline at end of file
diff --git a/src/cpp/heuristics/ValueHeuristicsManager.cpp b/src/cpp/heuristics/ValueHeuristicsManager.cpp
deleted file mode 100644
index 760d08566b6052f7d270853912e7fc201973b415..0000000000000000000000000000000000000000
--- a/src/cpp/heuristics/ValueHeuristicsManager.cpp
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * @author Tim Luchterhand
- * @date 06.05.24
- * @brief
- */
-
-#include <stdexcept>
-
-#include "heuristics/ValueHeuristicsManager.hpp"
-
-namespace tempo::heuristics {
-
-auto valHeuristicTypeToString(Options::PolarityHeuristic type) -> std::string {
-  using enum Options::PolarityHeuristic;
-  switch (type) {
-  case Tightest:
-    return "TightestValue";
-  case SolutionGuided:
-    return "SolutionGuided";
-  case Random:
-    return "RandomBinaryValue";
-  default:
-    throw std::runtime_error("unknown value heuristic type");
-  }
-}
-} // namespace tempo::heuristics
\ No newline at end of file
diff --git a/src/cpp/heuristics/heuristic_factories.cpp b/src/cpp/heuristics/heuristic_factories.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4e4d1f46847e6db6795f15094c9a802f781acc42
--- /dev/null
+++ b/src/cpp/heuristics/heuristic_factories.cpp
@@ -0,0 +1,40 @@
+/**
+* @author Tim Luchterhand
+* @date 09.07.24
+* @brief
+*/
+
+#include <stdexcept>
+
+#include "heuristics/heuristic_factories.hpp"
+
+#define RETURN_NAME(NAME) return #NAME
+
+namespace tempo::heuristics::detail {
+
+    auto getVarHName(Options::ChoicePointHeuristics heuristic) -> std::string {
+        switch (heuristic) {
+            case Options::ChoicePointHeuristics::Tightest:
+                RETURN_NAME(Tightest);
+            case Options::ChoicePointHeuristics::WeightedDegree:
+                RETURN_NAME(WeightedDegree);
+            case Options::ChoicePointHeuristics::VSIDS:
+                RETURN_NAME(VSIDS);
+            default:
+                throw std::runtime_error("unknown variable heuristic");
+        }
+    }
+
+    auto getValHName(Options::PolarityHeuristic heuristic) -> std::string {
+        switch (heuristic) {
+            case Options::PolarityHeuristic::Tightest:
+                RETURN_NAME(TightestValue);
+            case Options::PolarityHeuristic::SolutionGuided:
+                throw std::runtime_error("not implemented yet");
+            case Options::PolarityHeuristic::Random:
+                RETURN_NAME(RandomBinaryValue);
+            default:
+                throw std::runtime_error("unknown value heuristic type");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/cpp/nn/GNNEdgePolarityPredictor.cpp b/src/cpp/nn/GNNEdgePolarityPredictor.cpp
index 32eaf707e47fc709ebb28d6645e2437773fc30b5..8f601ce6059dbef48e58889c2efe3a0aa6bb9b4a 100644
--- a/src/cpp/nn/GNNEdgePolarityPredictor.cpp
+++ b/src/cpp/nn/GNNEdgePolarityPredictor.cpp
@@ -11,20 +11,12 @@
 
 namespace tempo::nn::heuristics {
 
-    GNNEdgePolarityPredictor::GNNEdgePolarityPredictor(const std::filesystem::path &modelLocation,
-                                                       const std::filesystem::path &featureExtractorConfigLocation,
-                                                       const ProblemInstance &problemInstance) :
-            gnn(modelLocation),
-            graphBuilder(featureExtractorConfigLocation, problemInstance) {}
-
-    bool GNNEdgePolarityPredictor::choosePolarityFromHeatMap(event from, event to, const Matrix<DataType> &heatMap) {
-        from = TASK(from);
-        to = TASK(to);
-        const auto edgeProb = heatMap(from, to);
-        const auto inverseProb = heatMap(to, from);
+    bool detail::choosePolarityFromHeatMap(unsigned taskFrom, unsigned taskTo, const Matrix<DataType> &heatMap) {
+        const auto edgeProb = heatMap(taskFrom, taskTo);
+        const auto inverseProb = heatMap(taskTo, taskFrom);
         if (edgeProb == GNN::NoValue or inverseProb == GNN::NoValue) {
             std::stringstream ss;
-            ss << "Invalid edge in GNN: task edge " << from << " -> " << to << std::endl;
+            ss << "Invalid edge in GNN: task edge " << taskFrom << " -> " << taskTo << std::endl;
             throw std::runtime_error(ss.str());
         }
 
diff --git a/src/cpp/nn/GraphBuilder.cpp b/src/cpp/nn/GraphBuilder.cpp
index e81b13ad4b468dc00e1d1247533da69b8ee9ccc6..ab9e9548f4afe04bd5191828600c63ebf62cf4c7 100644
--- a/src/cpp/nn/GraphBuilder.cpp
+++ b/src/cpp/nn/GraphBuilder.cpp
@@ -6,16 +6,4 @@
 #include "nn/GraphBuilder.hpp"
 
 namespace tempo::nn {
-
-    GraphBuilder::GraphBuilder(const std::filesystem::path &configPath, const ProblemInstance &problemInstance) {
-        auto data = serialization::deserializeFromFile<GraphBuilderConfig>(configPath);
-        topologyExtractor = TopologyBuilderFactory::getInstance().create(data.topologyExtractor.extractorName,
-                                                                         problemInstance);
-        taskFeatureExtractor = FeatureExtractorFactory::getInstance().create(
-                data.taskFeatureExtractor.extractorName, data.taskFeatureExtractor.arguments);
-        resourceFeatureExtractor = FeatureExtractorFactory::getInstance().create(
-                data.resourceFeatureExtractor.extractorName, data.resourceFeatureExtractor.arguments);
-        edgeFeatureExtractor = FeatureExtractorFactory::getInstance().create(
-                data.edgeFeatureExtractor.extractorName, data.edgeFeatureExtractor.arguments);
-    }
 }
\ No newline at end of file
diff --git a/src/cpp/nn/topology_extractors.cpp b/src/cpp/nn/topology_extractors.cpp
index c57d71a8ebfd4a172d7787f04b0e417baf745415..772bc658293d5842b72804ff01098de4bae55ea2 100644
--- a/src/cpp/nn/topology_extractors.cpp
+++ b/src/cpp/nn/topology_extractors.cpp
@@ -11,29 +11,8 @@
 
 namespace tempo::nn {
 
-    MinimalTopologyBuilder::MinimalTopologyBuilder(const ProblemInstance &problem) {
-        using namespace iterators;
-        cache.numTasks = problem.durations.size();
-        cache.numResources = problem.resources.size();
-        impl::TopologyData topologyData{.edgeLookup = impl::EdgeLookup(cache.numTasks * 2 + 2)};
-        for (auto [r, resourceSpec]: enumerate(problem.resources, 0l)) {
-            completeSubGraph(resourceSpec, r, topologyData);
-        }
-
-        addPrecedenceEdges(problem.constraints, topologyData);
-        cache.edgeIndices = util::makeIndexTensor(topologyData.edges);
-        cache.edgePairMask = torch::from_blob(topologyData.edgePairMask.data(),
-                                              static_cast<long>(topologyData.edgePairMask.size()),
-                                              indexTensorOptions()).clone();
-        cache.edgeResourceRelations = util::makeIndexTensor(topologyData.edgeIdx, topologyData.edgeRelResIdx);
-        cache.resourceDependencies = util::makeIndexTensor(topologyData.taskIdx, topologyData.resIdx);
-        cache.resourceDemands = torch::from_blob(topologyData.resDemands.data(),
-                                           {static_cast<long>(topologyData.taskIdx.size()), 1},
-                                           dataTensorOptions()).clone();
-    }
-
     auto MinimalTopologyBuilder::getTopology() const -> const Topology& {
-        return getTopology(makeSolverState(Matrix<int>{}));
+        return getTopology(makeSolverState(Matrix<int>{}, 0));
     }
 
     void MinimalTopologyBuilder::addEdge(const Edge &e, bool isResourceEdge, IndexType maskVal, impl::TopologyData &topologyData) {
@@ -48,32 +27,6 @@ namespace tempo::nn {
         topologyData.edgePairMask.emplace_back(maskVal);
     }
 
-    void MinimalTopologyBuilder::completeSubGraph(const Resource<int> &resourceSpec, IndexType resource,
-                                                  impl::TopologyData &topologyData) {
-        const auto &tasks = resourceSpec;
-        for (std::size_t i = 0; i < tasks.size(); ++i) {
-            topologyData.taskIdx.emplace_back(tasks[i]);
-            topologyData.resIdx.emplace_back(resource);
-            topologyData.resDemands.emplace_back(static_cast<DataType>(resourceSpec.getDemand(i)) /
-                                                 static_cast<DataType>(resourceSpec.capacity));
-            for (std::size_t j = i + 1; j < tasks.size(); ++j) {
-                Edge e(tasks[i], tasks[j]);
-                Edge rev(tasks[j], tasks[i]);
-                topologyData.edgeRelResIdx.emplace_back(resource);
-                topologyData.edgeRelResIdx.emplace_back(resource);
-                if (topologyData.edgeLookup.contains(e)) {
-                    topologyData.edgeIdx.emplace_back(topologyData.edgeLookup(e));
-                    topologyData.edgeIdx.emplace_back(topologyData.edgeLookup(rev));
-                    continue;
-                }
-
-                addEdge(e, true, topologyData.pairMaskVal, topologyData);
-                addEdge(rev, true, topologyData.pairMaskVal, topologyData);
-                ++topologyData.pairMaskVal;
-            }
-        }
-    }
-
     std::size_t impl::EdgeLookup::getNumEdges() const {
         std::size_t ret = 0;
         this->for_each([&ret](auto val) { ret += (val != EdgeLookup::NoValue); });
diff --git a/src/cpp/util/Options.cpp b/src/cpp/util/Options.cpp
index a1ba62d6d9f1c8d5274907a44ef8e973cd862dac..dac328eb398402463346f75ccffcf23b7a50b88e 100755
--- a/src/cpp/util/Options.cpp
+++ b/src/cpp/util/Options.cpp
@@ -7,202 +7,184 @@
 
 // using namespace schedcl;
 
-struct argbase {
-  virtual ~argbase() {}
-  virtual void assign() = 0;
-};
-
-template <typename Opt, typename ClapArg, typename E = void>
-struct arg : public argbase {
-  ClapArg carg;
-  Opt &opt;
-
-  template <typename... T>
-  arg(TCLAP::CmdLine &cmd, Opt &opt, T &&...args)
-      : carg(std::forward<T>(args)...), opt(opt) {
-    cmd.add(carg);
-  }
-
-  virtual void assign() override { opt = carg.getValue(); }
-};
-
-template <typename Opt, typename ClapArg>
-struct arg<Opt, ClapArg, typename std::enable_if<std::is_enum<Opt>{}>::type>
-    : public argbase {
-  ClapArg carg;
-  Opt &opt;
-
-  template <typename... T>
-  arg(TCLAP::CmdLine &cmd, Opt &opt, T &&...args)
-      : carg(std::forward<T>(args)...), opt(opt) {
-    cmd.add(carg);
-  }
-
-  virtual void assign() override {
-    opt =
-        static_cast<typename std::remove_reference<Opt>::type>(carg.getValue());
-  }
-};
-
-struct cmdline {
-  TCLAP::CmdLine cmd;
-  std::vector<std::unique_ptr<argbase>> args;
-
-  cmdline(const std::string &message, const char delimiter = ' ',
-          const std::string &version = "none", bool helpAndVersion = true)
-      : cmd(message, delimiter, version, helpAndVersion) {}
-
-  template <typename ClapArg, typename Opt, typename... T>
-  void add(Opt &opt, T &&...clapargs) {
-    args.emplace_back(std::move(std::make_unique<arg<Opt, ClapArg>>(
-        cmd, opt, std::forward<T>(clapargs)...)));
-  }
-
-  void parse(int argc, char *argv[]) {
-    cmd.parse(argc, argv);
-    for (auto &arg : args)
-      arg->assign();
-  }
-};
 
 tempo::Options tempo::parse(int argc, char *argv[]) {
   using namespace TCLAP;
   using namespace std::string_literals;
-  cmdline cmd("schedcl", ' ');
-
-  Options opt;
-  opt.cmdline =
-      accumulate(argv, argv + argc, ""s, [&](std::string acc, const char *arg) {
-        return acc + " " + arg;
-      });
-
-  cmd.add<UnlabeledValueArg<std::string>>(opt.instance_file, "file",
-                                          "instance file", true, "", "string");
-
-  cmd.add<ValueArg<int>>(
-      opt.verbosity, "", "verbosity",
-      "verbosity level (0:silent,1:quiet,2:improvements only,3:verbose", false,
-      2, "int");
-
-  cmd.add<ValueArg<int>>(opt.seed, "", "seed", "random seed", false, 1, "int");
-
-  cmd.add<ValueArg<int>>(opt.ub, "", "ub", "initial ub", false,
-                         std::numeric_limits<int>::max(), "int");
-
-  cmd.add<SwitchArg>(opt.print_sol, "", "print-sol",
-                     "print the best found schedule", false);
-
-  cmd.add<SwitchArg>(opt.print_par, "", "print-par", "print the paramters",
-                     false);
-
-  cmd.add<SwitchArg>(opt.print_mod, "", "print-mod", "print the model", false);
-
-  cmd.add<SwitchArg>(opt.print_ins, "", "print-ins", "print the instance",
-                     false);
-
-  cmd.add<SwitchArg>(opt.print_sta, "", "print-sta", "print the statistics",
-                     false);
-
-  cmd.add<SwitchArg>(opt.print_cmd, "", "print-cmd", "print the command-line",
-                     false);
-
-  cmd.add<ValueArg<std::string>>(opt.dbg_file, "", "dbg",
-                                 "Clause-learning dbg file []", false, "",
-                                 "string");
-
-  cmd.add<SwitchArg>(opt.learning, "", "learning", "learn clauses", false);
-  cmd.add<SwitchArg>(opt.learning, "", "no-learning",
-                     "do not use clause learning", true);
-
-  cmd.add<SwitchArg>(opt.edge_finding, "", "edge-finding", "use edge-finding",
-                     false);
-  cmd.add<SwitchArg>(opt.edge_finding, "", "no-edge-finding",
-                     "do not use edge-finding", true);
-
-  cmd.add<SwitchArg>(opt.transitivity, "", "transitivity",
-                     "use transitivity reasoning", false);
-  cmd.add<SwitchArg>(opt.transitivity, "", "no-transitivity",
-                     "do not use transitivity reasoning", true);
-
-  cmd.add<SwitchArg>(opt.dichotomy, "", "dichotomy", "use dichotomic search",
-                     false);
-
-  cmd.add<SwitchArg>(opt.full_up, "", "full-up",
-                     "unit-propagate bound literals", false);
-
-  cmd.add<SwitchArg>(opt.order_bound_watch, "", "order-watched",
-                     "order bound watched lists", false);
-
-  cmd.add<ValueArg<int>>(opt.choice_point_heuristics, "", "cp-heuristic",
-                         "type of heuristic used for choice point selection "
-                         "(0: Tightest, 1: WDEG, 2: VSIDS (default))",
-                         false, 2, "int");
-  cmd.add<ValueArg<int>>(
-      opt.polarity_heuristic, "", "polarity-heuristic",
-      "type "
-      "of heuristic used for choice point polarity selection "
-      "(0: tightest (default), 1: solution guided, 2: random)",
-      false, 0, "int");
-
-  cmd.add<ValueArg<double>>(
-      opt.polarity_epsilon, "", "polarity-epsilon",
-      "epsilon greedy value for value selection. probability in [0, 1]."
-      "0 means that the value selected by the heuristic is always"
-      "chosen, 1 means always random. default: 0.01",
-      false, 0.01, "double");
-
-  cmd.add<ValueArg<double>>(
-      opt.vsids_decay, "", "vsids-decay",
-      "decay value for the vsids heuristic, only effective if VSIDS is used "
-      "as choice point heuristic",
-      false, 0.999, "double");
-
-  cmd.add<ValueArg<double>>(opt.forgetfulness, "", "forgetfulness",
-                            "clause base reduction factor (0.3)", false, 0.3,
-                            "double");
-
-  cmd.add<ValueArg<int>>(
-      opt.minimization, "", "clause-minimization",
-      "depth for clause minimization (default 1)",
-      false, 1, "int");
-    
+  Parser p = getBaseParser();
+  p.getOptions().cmdline =
+          accumulate(argv, argv + argc, ""s, [&](const std::string& acc, const char *arg) {
+              return acc + " " + arg;
+          });
+
+
+  p.parse(argc, argv);
+  return p.getOptions();
+}
+
+auto tempo::getBaseParser() -> Parser {
+    using namespace TCLAP;
+    Parser p;
+    auto &cmd = p.getCmdLine();
+    auto &opt = p.getOptions();
+    cmd.add<UnlabeledValueArg<std::string>>(opt.instance_file, "file",
+                                            "instance file", true, "", "string");
+
+    cmd.add<ValueArg<int>>(
+            opt.verbosity, "", "verbosity",
+            "verbosity level (0:silent,1:quiet,2:improvements only,3:verbose", false,
+            2, "int");
+
+    cmd.add<ValueArg<int>>(opt.seed, "", "seed", "random seed", false, 1, "int");
+
+    cmd.add<ValueArg<int>>(opt.ub, "", "ub", "initial ub", false,
+                           std::numeric_limits<int>::max(), "int");
+
+    cmd.add<SwitchArg>(opt.print_sol, "", "print-sol",
+                       "print the best found schedule", false);
+
+    cmd.add<SwitchArg>(opt.print_par, "", "print-par", "print the paramters",
+                       false);
+
+    cmd.add<SwitchArg>(opt.print_mod, "", "print-mod", "print the model", false);
+
+    cmd.add<SwitchArg>(opt.print_ins, "", "print-ins", "print the instance",
+                       false);
+
+    cmd.add<SwitchArg>(opt.print_sta, "", "print-sta", "print the statistics",
+                       false);
+
+    cmd.add<SwitchArg>(opt.print_cmd, "", "print-cmd", "print the command-line",
+                       false);
+
+    cmd.add<ValueArg<std::string>>(opt.dbg_file, "", "dbg",
+                                   "Clause-learning dbg file []", false, "",
+                                   "string");
+
+    cmd.add<SwitchArg>(opt.learning, "", "learning", "learn clauses", false);
+    cmd.add<SwitchArg>(opt.learning, "", "no-learning",
+                       "do not use clause learning", true);
+
+    cmd.add<SwitchArg>(opt.edge_finding, "", "edge-finding", "use edge-finding",
+                       false);
+    cmd.add<SwitchArg>(opt.edge_finding, "", "no-edge-finding",
+                       "do not use edge-finding", true);
+
+    cmd.add<SwitchArg>(opt.transitivity, "", "transitivity",
+                       "use transitivity reasoning", false);
+    cmd.add<SwitchArg>(opt.transitivity, "", "no-transitivity",
+                       "do not use transitivity reasoning", true);
+
+    cmd.add<SwitchArg>(opt.dichotomy, "", "dichotomy", "use dichotomic search",
+                       false);
+
+    cmd.add<SwitchArg>(opt.full_up, "", "full-up",
+                       "unit-propagate bound literals", false);
+
+    cmd.add<SwitchArg>(opt.order_bound_watch, "", "order-watched",
+                       "order bound watched lists", false);
+
+    cmd.add<ValueArg<int>>(opt.choice_point_heuristics, "", "cp-heuristic",
+                           "type of heuristic used for choice point selection "
+                           "(0: Tightest, 1: WDEG, 2: VSIDS (default))",
+                           false, 2, "int");
     cmd.add<ValueArg<int>>(
-        opt.greedy_runs, "", "greedy-runs",
-        "number of randomized greedy runs (default 1)",
-        false, 1, "int");
-
-  cmd.add<ValueArg<int>>(opt.forget_strategy, "", "forget-strategy",
-                         "strategy for clause forgetting "
-                         "(0: size (default), 1: literal looseness,"
-                         "2: literal activity 3: looseness / activity",
-                         false, 3, "int");
-
-  cmd.add<ValueArg<std::string>>(opt.restart_policy, "", "restart",
-                                 "choice of restart policy (no, luby, geom)",
-                                 false, "geom", "string");
-
-  cmd.add<ValueArg<int>>(
-      opt.restart_base, "", "restart-base",
-      "base of the sequence for geometric/luby restarts (default=100)", false,
-      128, "int");
-
-  cmd.add<ValueArg<double>>(opt.restart_factor, "", "restart-factor",
-                            "factor of the geometric sequence (default=1.2)",
-                            false, 1.2, "double");
-
-  cmd.add<ValueArg<double>>(opt.vsids_epsilon, "", "vsids-epsilon",
-                            "epsilon value for the epsilon greedy vsids "
-                            "heuristic, only effective if epsilon greedy "
-                            "VSIDS is used as choice point heuristic",
-                            false, 0.05, "double");
-
-  cmd.add<ValueArg<std::string>>(
-      opt.input_format, "", "input-format",
-      "format of input file "
-      "(osp: Open Shop (default), jsp: Job Shop "
-      "(Lawrence), tsptw: TSP with Time Windows, jstl: JSP with Time Lags)",
-      false, "osp", "string");
-
-  cmd.parse(argc, argv);
-  return opt;
+            opt.polarity_heuristic, "", "polarity-heuristic",
+            "type "
+            "of heuristic used for choice point polarity selection "
+            "(0: tightest (default), 1: solution guided, 2: random)",
+            false, 0, "int");
+
+    cmd.add<ValueArg<double>>(
+            opt.polarity_epsilon, "", "polarity-epsilon",
+            "epsilon greedy value for value selection. probability in [0, 1]."
+            "0 means that the value selected by the heuristic is always"
+            "chosen, 1 means always random. default: 0.01",
+            false, 0.01, "double");
+
+    cmd.add<ValueArg<double>>(
+            opt.vsids_decay, "", "vsids-decay",
+            "decay value for the vsids heuristic, only effective if VSIDS is used "
+            "as choice point heuristic",
+            false, 0.999, "double");
+
+    cmd.add<ValueArg<double>>(opt.forgetfulness, "", "forgetfulness",
+                              "clause base reduction factor (0.3)", false, 0.3,
+                              "double");
+
+    cmd.add<ValueArg<int>>(
+            opt.minimization, "", "clause-minimization",
+            "depth for clause minimization (default 1)",
+            false, 1, "int");
+
+    cmd.add<ValueArg<int>>(
+            opt.greedy_runs, "", "greedy-runs",
+            "number of randomized greedy runs (default 1)",
+            false, 1, "int");
+
+    cmd.add<ValueArg<int>>(opt.forget_strategy, "", "forget-strategy",
+                           "strategy for clause forgetting "
+                           "(0: size (default), 1: literal looseness,"
+                           "2: literal activity 3: looseness / activity",
+                           false, 3, "int");
+
+    cmd.add<ValueArg<std::string>>(opt.restart_policy, "", "restart",
+                                   "choice of restart policy (no, luby, geom)",
+                                   false, "geom", "string");
+
+    cmd.add<ValueArg<int>>(
+            opt.restart_base, "", "restart-base",
+            "base of the sequence for geometric/luby restarts (default=100)", false,
+            128, "int");
+
+    cmd.add<ValueArg<double>>(opt.restart_factor, "", "restart-factor",
+                              "factor of the geometric sequence (default=1.2)",
+                              false, 1.2, "double");
+
+    cmd.add<ValueArg<double>>(opt.vsids_epsilon, "", "vsids-epsilon",
+                              "epsilon value for the epsilon greedy vsids "
+                              "heuristic, only effective if epsilon greedy "
+                              "VSIDS is used as choice point heuristic",
+                              false, 0.05, "double");
+
+    cmd.add<ValueArg<std::string>>(
+            opt.input_format, "", "input-format",
+            "format of input file "
+            "(osp: Open Shop (default), jsp: Job Shop "
+            "(Lawrence), tsptw: TSP with Time Windows, jstl: JSP with Time Lags)",
+            false, "osp", "string");
+    return p;
+}
+
+cmdline::cmdline(const std::string &message, const char delimiter, const std::string &version, bool helpAndVersion)
+        : cmd(message, delimiter, version, helpAndVersion) {}
+
+void cmdline::parse(int argc, char **argv) {
+    cmd.parse(argc, argv);
+    for (auto &arg: args) {
+        arg->assign();
+    }
+}
+
+tempo::Parser::Parser(const std::string &name) : options(std::make_unique<Options>()),
+                                                 cmdLine(std::make_unique<cmdline>(name, ' ')) {}
+
+auto tempo::Parser::getOptions() noexcept -> tempo::Options & {
+    return *options;
+}
+
+auto tempo::Parser::getOptions() const noexcept -> const tempo::Options & {
+    return *options;
+}
+
+auto tempo::Parser::getCmdLine() noexcept -> cmdline & {
+    return *cmdLine;
+}
+
+auto tempo::Parser::getCmdLine() const noexcept -> const cmdline & {
+    return *cmdLine;
+}
+
+void tempo::Parser::parse(int argc, char **argv) {
+    cmdLine->parse(argc, argv);
 }
diff --git a/src/cpp/util/SchedulingProblemHelper.cpp b/src/cpp/util/SchedulingProblemHelper.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..df0bf641623794d8a2fce7f544fa52173781e6d5
--- /dev/null
+++ b/src/cpp/util/SchedulingProblemHelper.cpp
@@ -0,0 +1,19 @@
+/**
+* @author Tim Luchterhand
+* @date 01.07.24
+* @brief
+*/
+
+#include "util/SchedulingProblemHelper.hpp"
+
+namespace tempo{
+
+    unsigned VarTaskMapping::operator()(var_t variable) const noexcept {
+        return varToTask[variable - offset];
+    }
+
+    bool VarTaskMapping::contains(var_t variable) const noexcept {
+        const var_t idx = variable - offset;
+        return idx < varToTask.size() and varToTask[idx] != NoTask;
+    }
+}
\ No newline at end of file
diff --git a/src/examples/checkcl.cpp b/src/examples/checkcl.cpp
deleted file mode 100644
index fc05e990ccd544fe1be04d0867d5c4bf859af3a8..0000000000000000000000000000000000000000
--- a/src/examples/checkcl.cpp
+++ /dev/null
@@ -1,191 +0,0 @@
-/*************************************************************************
-minicsp
-
-Copyright 2010--2011 George Katsirelos
-
-Minicsp is free software: you can redistribute it and/or modify it
-under the terms of the GNU General Public License as published by the
-Free Software Foundation, either version 3 of the License, or (at your
-option) any later version.
-
-Minicsp is distributed in the hope that it will be useful, but WITHOUT
-ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
-for more details.
-
-You should have received a copy of the GNU General Public License
-along with minicsp.  If not, see <http://www.gnu.org/licenses/>.
-
-*************************************************************************/
-
-
-#include <chrono>
-#include <iostream>
-#include <vector>
-#include <filesystem>
-#include <optional>
-
-#include "Scheduler.hpp"
-#include "util/parsing/format.hpp"
-#include "util/parsing/jsp.hpp"
-#include "util/parsing/osp.hpp"
-
-
-using namespace tempo;
-
-
-int main(int argc, char *argv[]) {
-  Options opt = tempo::parse(argc, argv);
-
-  seed(opt.seed);
-
-  ProblemInstance data;
-
-  size_t num_fails{0};
-  size_t num_trivial{0};
-  size_t num_search{0};
-
-  if (opt.input_format == "osp") {
-    data = osp::read_instance(opt.instance_file);
-  }
-  else if (opt.input_format == "jsp") {
-    data = jsp::read_instance(opt.instance_file);
-  }
-
-  Scheduler<int> S(opt);
-  for (auto d : data.durations) {
-    S.newTask(d, d);
-  }
-    
-  for (auto [x, y, d] : data.constraints) {
-
-      assert(d==0);
-
-    S.newPrecedence(x, y, d);
-
-  }
-    
-  for (auto &job : data.resources) {
-      for (auto ti{job.begin()}; ti!=job.end(); ++ti) {
-          for (auto tj{ti+1}; tj!=job.end(); ++tj) {
-              S.newVariable({START(*ti), END(*tj), 0}, {START(*tj), END(*ti), 0});
-          }
-      }
-  }
-    
-//    S.display(std::cout, true, true);
-
-//    std::cout << "\nsave:\n" << S << std::endl;
-
-  std::ifstream cl_file("mcl.txt", std::ifstream::in);
-
-  int t, n, x, y, d;
-
-  std::vector<int> X;
-  std::vector<int> Y;
-  std::vector<int> D;
-
-  int line{0};
-  do {
-
-    X.clear();
-    Y.clear();
-    D.clear();
-
-    S.saveState();
-
-    cl_file >> t;
-    cl_file >> n;
-    if (not cl_file.good())
-      break;
-
-    bool trivially_unsat{false};
-
-    for (auto i{0}; i < n; ++i) {
-      cl_file >> x;
-      cl_file >> y;
-      cl_file >> d;
-
-      X.push_back(x);
-      Y.push_back(y);
-      D.push_back(d);
-
-      //            std::cout << "add " << prettyEvent(y) << " - " <<
-      //            prettyEvent(x) << " <= " << d << std::endl;
-
-      try {
-        S.newMaximumLag(x, y, d);
-      } catch (Failure &f) {
-        trivially_unsat = true;
-      }
-    }
-
-    //        std::cout << "\nsolve:\n" << S << std::endl;
-
-    if (not trivially_unsat) {
-      //            std::cout << "ok (trivial)\n";
-      //        } else {
-
-      //            std::cout << S << std::endl;
-      //            exit(1);
-
-      bool need_search{true};
-      try {
-        S.propagate();
-      } catch (Failure &f) {
-        need_search = false;
-      }
-
-      if (need_search) {
-
-        //          std::cout << S << std::endl;
-
-        auto nf{S.num_fails};
-        S.search();
-
-        if (S.satisfiable()) {
-          std::cout << "cl " << line << " (" << (t ? "expl" : "cut") << "): ";
-          std::cout << "bug!\n";
-
-          for (size_t i{0}; i < X.size(); ++i) {
-            std::cout << "> " << prettyEvent(Y[i]) << " - " << prettyEvent(X[i])
-                      << " <= " << D[i] << std::endl;
-          }
-
-          exit(1);
-        }
-        //                else {
-        //                    std::cout << "UNSAT\n";
-        //                }
-
-        ++num_search;
-        num_fails += (S.num_fails - nf);
-      }
-      //            else {
-      //                std::cout << "ok\n";
-      //            }
-    } else {
-      ++num_trivial;
-    }
-    ++line;
-
-    //        std::cout << S.env.level() << " --> 0"
-
-    S.restoreState(0);
-
-    //        std::cout << "\nrestore:\n" << S << std::endl;
-
-    //    if ((line % 100) == 0)
-    std::cout << line << ": " << num_trivial << " trivial, "
-              << (line - num_trivial - num_search) << " easy, " << num_search
-              << " hard (" << num_fails / num_search << ")" << std::endl;
-
-    //        if(line > 100)
-    //            exit(1);
-    } while(true);
-    
-    
-    
- 
- 
-}
diff --git a/src/examples/testbounds.cpp b/src/examples/testbounds.cpp
deleted file mode 100644
index 0d80e419de4b84a7220db352be8b4f6e2f97a1e5..0000000000000000000000000000000000000000
--- a/src/examples/testbounds.cpp
+++ /dev/null
@@ -1,81 +0,0 @@
-/*************************************************************************
-minicsp
-
-Copyright 2010--2011 George Katsirelos
-
-Minicsp is free software: you can redistribute it and/or modify it
-under the terms of the GNU General Public License as published by the
-Free Software Foundation, either version 3 of the License, or (at your
-option) any later version.
-
-Minicsp is distributed in the hope that it will be useful, but WITHOUT
-ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
-for more details.
-
-You should have received a copy of the GNU General Public License
-along with minicsp.  If not, see <http://www.gnu.org/licenses/>.
-
-*************************************************************************/
-
-
-#include <iostream>
-
-
-#include "Scheduler.hpp"
-
-
-using namespace tempo;
-
-
-
-
-int main(int argc, char *argv[]) {
-    
-    int n = 5;
-    
-    Options opt = tempo::parse(argc, argv);
-    Scheduler<int> s_(opt);
-    
-    BoundSystem<int>& S{s_.domain.bounds};
-    
-    S.resize(n);
-    
-    std::cout << S << std::endl;
-    
-    for(auto l{0}; l<10; ++l) {
-        
-        ReversibleObject::env->save();
-        
-        try {
-            for(auto i{0}; i<3; ++i) {
-                
-                event a{static_cast<event>(tempo::random()%n)};
-                int k{static_cast<int>(tempo::random()%10000)};
-                
-                event b{static_cast<event>(tempo::random()%n)};
-                int l{static_cast<int>(tempo::random()%100)};
-                
-                std::cout << "set " << prettyEvent(a) << " <= " << k << std::endl;
-                
-                S.set(UPPER, a, k);
-                
-                std::cout << "set " << prettyEvent(b) << " >= " << l << std::endl;
-                
-                S.set(LOWER, b, -l);
-            }
-            std::cout << std::endl << S << std::endl;
-            
-        } catch(std::exception& e) {
-            std::cout << "negative cycle:\n" << S << std::endl;
-            break;
-        }
-    }
-    
-    while(ReversibleObject::env->level() > 0) {
-        ReversibleObject::env->restore(ReversibleObject::env->level()-1);
-        std::cout << std::endl << S << std::endl;
-    }
-  
- 
-}
diff --git a/src/examples/testsearch.cpp b/src/examples/testsearch.cpp
deleted file mode 100644
index 907867c2f32dbadf8ea48f1941385163218ea153..0000000000000000000000000000000000000000
--- a/src/examples/testsearch.cpp
+++ /dev/null
@@ -1,252 +0,0 @@
-/*************************************************************************
-minicsp
-
-Copyright 2010--2011 George Katsirelos
-
-Minicsp is free software: you can redistribute it and/or modify it
-under the terms of the GNU General Public License as published by the
-Free Software Foundation, either version 3 of the License, or (at your
-option) any later version.
-
-Minicsp is distributed in the hope that it will be useful, but WITHOUT
-ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
-for more details.
-
-You should have received a copy of the GNU General Public License
-along with minicsp.  If not, see <http://www.gnu.org/licenses/>.
-
-*************************************************************************/
-
-
-#include <chrono>
-#include <iostream>
-#include <vector>
-#include <filesystem>
-#include <optional>
-
-//#include "Objective.hpp"
-#include "Scheduler.hpp"
-#include "util/parsing/format.hpp"
-#include "util/parsing/jsp.hpp"
-#include "util/parsing/jstl.hpp"
-#include "util/parsing/osp.hpp"
-#include "util/parsing/tsptw.hpp"
-
-using namespace tempo;
-
-void load(std::string &solfile_name, std::vector<bool> &solution) {
-  //    std::cout << "load\n";
-
-  std::ifstream solfile(solfile_name.c_str(), std::ifstream::in);
-  size_t n;
-  bool val;
-  solfile >> n;
-
-  //    std::cout << n << std::endl;
-
-  solution.resize(n);
-
-  for (size_t i{0}; i < n; ++i) {
-    solfile >> val;
-
-    //        std::cout << val << std::endl;
-
-    solution[i] = val;
-  }
-}
-
-int main(int argc, char *argv[]) {
-
-  auto start = std::chrono::system_clock::now();
-  Options opt = tempo::parse(argc, argv);
-
-  seed(opt.seed);
-
-  ProblemInstance data;
-
-  int ub{opt.ub};
-
-//    std::cout << ub << " <> " << INFTY << std::endl;
-    
-  if (opt.input_format == "osp") {
-    data = osp::read_instance(opt.instance_file);
-    if (ub == INFTY)
-      ub = osp::getUb<int>(data);
-    else {
-        std::cout << ub << " <> " << INFTY << std::endl;
-        exit(1);
-    }
-  }
-  else if (opt.input_format == "jsp") {
-    data = jsp::read_instance(opt.instance_file);
-    if (ub == INFTY)
-      ub = jsp::getUb<int>(data);
-  } else if (opt.input_format == "tsptw") {
-    data = tsptw::read_instance(opt.instance_file);
-  } else if (opt.input_format == "jstl") {
-    data = jstl::read_instance(opt.instance_file);
-    if (ub == INFTY)
-      ub = jstl::getUb<int>(data);
-  }
-
-  if (opt.print_ins) {
-    std::cout << data.durations.size() << " tasks:";
-    for (auto d : data.durations) {
-      std::cout << " " << d;
-    }
-    std::cout << std::endl;
-
-    std::cout << data.resources.size() << " resources:";
-    for (auto R : data.resources) {
-      std::cout << " (";
-      for (auto x : R) {
-        std::cout << " " << x;
-      }
-      std::cout << ")";
-    }
-    std::cout << std::endl << "ub = " << ub << std::endl;
-  }
-
-  Scheduler<int> S(opt);
-
-  //  std::vector<task> t;
-  //  for (auto d : data.durations) {
-  //    t.push_back(S.newTask(d, d));
-  //  }
-  //
-  //  //  S.setUpperBound(ub);
-  //
-  //  for (auto [x, y, k] : data.constraints) {
-  //
-  //    S.newPrecedence(x, y, k);
-  //  }
-  //
-  //  //  std::cout << data.resources.size() << std::endl;
-  //  //  std::cout << data.resources[0].size() << std::endl;
-  //  //  //    exit(1);
-  //
-  //  std::vector<var> scope;
-  //  std::vector<var> tasks;
-  //    std::vector<Task<int>> the_tasks;
-  //  for (auto &job : data.resources) {
-  //    for (size_t i{0}; i < job.size(); ++i) {
-  //      tasks.push_back(t[job[i]]);
-  //        the_tasks.push_back(S.getTask(t[job[i]]));
-  //      for (size_t j{i + 1}; j < job.size(); ++j) {
-  //        scope.push_back(S.newVariable(
-  //            {START(t[job[i]]), END(t[job[j]]), -job.getTransitionTime(j,
-  //            i)}, {START(t[job[j]]), END(t[job[i]]),
-  //            -job.getTransitionTime(i, j)}));
-  //      }
-  //    }
-  //
-  //    //           for (auto ti{job.begin()}; ti!=job.end(); ++ti) {
-  //    //          for (auto tj{ti+1}; tj!=job.end(); ++tj) {
-  //    //            scope.push_back(S.newVariable({START(*ti), END(*tj), 0},
-  //    //                                          {START(*tj), END(*ti), 0}));
-  //    //          }
-  //    //      }
-  //    if (opt.edge_finding) {
-  //      S.postEdgeFinding(tasks.begin(), tasks.end(), the_tasks.begin(),
-  //      the_tasks.end(), scope.begin(), scope.end());
-  //    }
-  //      if (opt.transitivity) {
-  //        S.postTransitivityReasoning(tasks.begin(), tasks.end(),
-  //        scope.begin(),
-  //                                    scope.end());
-  //      }
-  //      the_tasks.clear();
-  //      tasks.clear();
-  //      scope.clear();
-  //  }
-
-  S.load(data);
-
-#ifdef DBG_SOL
-  if (opt.dbg_file != "") {
-    std::vector<bool> dbg_sol;
-    load(opt.dbg_file, dbg_sol);
-    S.load(dbg_sol);
-    //    std::cout << "load";
-    //    for (size_t i{0}; i < dbg_sol.size(); ++i)
-    //      std::cout << " " << dbg_sol[i];
-    //    std::cout << std::endl;
-  }
-#endif
-
-  //    std::cout << S << std::endl;
-
-  //  S.search();
-
-
-
-  int sol;
-  //  if (opt.input_format == "tsptw") {
-  //    //        PathLength<int> tour(S, data.resources[0]);
-  //    //        S.minimize(tour);
-  //    //        sol = tour.upperBound();
-  //
-  //      if (opt.print_mod) {
-  //        S.display(std::cout, true, true, true, true, false, false, true);
-  //        std::cout << S << std::endl;
-  //      }
-  //
-  //    sol = S.satisfiable();
-  //      std::cout << (sol ? "SAT" : "UNSAT") << std::endl;
-  //  } else {
-  Makespan<int> makespan(S, ub);
-
-  if (opt.print_mod) {
-    S.display(std::cout, true, true, true, true, false, false, true);
-    std::cout << S << std::endl;
-  }
-
-    //      makespan.setUpperBound(ub);
-    if (opt.dichotomy)
-      S.optimize_dichotomy(makespan);
-    else
-      S.optimize(makespan);
-    
-    sol = makespan.primalBound();
-    //  }
-
-    if (opt.verbosity > tempo::Options::SILENT) {
-      auto end = std::chrono::system_clock::now();
-      auto elapsed =
-          std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
-      std::cout << "Objective value = " << sol
-                << " Execution time = " << elapsed.count() << " ms"
-                << std::endl;
-    }
-
-  //  auto sol = S.getMakespan();
-  //  auto sol = makespan.upperBound();
-  if (KillHandler::instance().signalReceived()) {
-    std::cout << "execution aborted, best solution found " << sol << std::endl;
-  } else if (data.optimalSolution != -1 and sol != data.optimalSolution) {
-    std::cerr << "Suboptimal solution! " << sol << " vs optimal "
-              << data.optimalSolution << std::endl;
-    std::exit(1);
-  }
-
-  //    vector<task>  the_tasks;
-  //
-  //    vector<vector<int>> order;
-  //
-  //    //order[i][j] -> indice de newVariable()
-  //
-  //    sort(the_tasks.begin(), the_tasks.end(), [](const task a, const task b)
-  //    {
-  //        return before[oder[a][b]];
-  //    };
-  //         );
-
-  if (opt.print_sol) {
-    auto before{S.getSolution()};
-    std::cout << before.size();
-    for (size_t i{0}; i < before.size(); ++i)
-      std::cout << " " << before[i];
-    std::cout << std::endl;
-  }
-}
diff --git a/src/examples/torch/scheduler_gnn.cpp b/src/examples/torch/scheduler_gnn.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..61e47a4bb9b020e1a96e62d4555aa006f10cc39d
--- /dev/null
+++ b/src/examples/torch/scheduler_gnn.cpp
@@ -0,0 +1,73 @@
+/**
+* @author Tim Luchterhand
+* @date 10.07.24
+* @brief
+*/
+
+#include <vector>
+
+#include "Solver.hpp"
+#include "heuristics/GNNValueHeuristics.hpp"
+#include "heuristics/heuristic_factories.hpp"
+#include "util/SchedulingProblemHelper.hpp"
+#include "util/parsing/jsp.hpp"
+#include "util/parsing/osp.hpp"
+
+
+template<typename T = int>
+class DisjunctiveResource : public vector<tempo::Interval<T>> {
+public:
+    using vector<tempo::Interval<T>>::vector;
+
+    static constexpr auto resourceCapacity() noexcept { return 1; }
+
+    static constexpr auto getDemand(unsigned) noexcept { return 1; }
+};
+
+
+
+int main(int argc, char **argv) {
+    using namespace tempo;
+    using namespace heuristics;
+    Parser p = getBaseParser();
+    std::string gnnLocation;
+    std::string featureExtractorConf;
+    p.getCmdLine().add<TCLAP::ValueArg<std::string>>(gnnLocation, "", "gnn-loc", "Location of the trained GNN", true,
+                                                     "", "string");
+    p.getCmdLine().add<TCLAP::ValueArg<std::string>>(featureExtractorConf, "", "feat-config",
+                                                     "Location of the feature extractor config", true, "", "string");
+    p.parse(argc, argv);
+    const auto &opt = p.getOptions();
+    Solver<> S(opt);
+    auto schedule{S.newInterval(0, Constant::Infinity<int>, 0, 0, 0, Constant::Infinity<int>)};
+    std::vector<DisjunctiveResource<>> resources;
+    std::vector<Interval<>> tasks;
+
+    if (opt.input_format == "osp") {
+        osp::parse(opt.instance_file, S, schedule, tasks, resources);
+    } else {
+        std::cerr << "problem type " << opt.input_format << " is not (yet) supported" << std::endl;
+        std::exit(1);
+    }
+
+    SchedulingProblemHelper problem(std::move(tasks), std::move(resources), {}, schedule);
+
+    for (const auto &consumingTasks: problem.resources()) {
+        auto constraint = NoOverlap(schedule, consumingTasks);
+        S.post(constraint);
+    }
+
+    auto trivialUb = 0;
+    for (const auto &t : problem.tasks()) {
+        trivialUb += t.minDuration(S);
+    }
+
+    trivialUb = std::min(trivialUb, opt.ub);
+    S.set(schedule.end.before(trivialUb));
+
+    GNNFullGuidance valBranching(opt.polarity_epsilon, gnnLocation, featureExtractorConf, std::move(problem));
+    auto varBranching = make_variable_heuristic(S);
+    S.setBranchingHeuristic(make_compound_heuristic(std::move(varBranching), std::move(valBranching)));
+    S.minimize(schedule.end);
+    return 0;
+}
\ No newline at end of file
diff --git a/src/header/Global.hpp b/src/header/Global.hpp
index 945cc674d5b2f344e9a99b2c345597321929809d..afbe0d10ac67bfdc732e68ee0338741f2b8927e6 100644
--- a/src/header/Global.hpp
+++ b/src/header/Global.hpp
@@ -80,13 +80,6 @@ enum class Priority {
     High
 };
 
-enum class VariableType {
-    Boolean,
-    Numeric
-};
-
-using VariableSelection = std::pair<var_t, VariableType>;
-
 /**
  * Converts enum to underlying type
  * @tparam E enum type
diff --git a/src/header/Model.hpp b/src/header/Model.hpp
index d3fada8db0225e7cb7ed0953d0e76a6b46319eca..e04b82b0f8ddc272c0d6a8fa7d686a3dfb32cf81 100755
--- a/src/header/Model.hpp
+++ b/src/header/Model.hpp
@@ -24,6 +24,7 @@
 //#define DBG_EXTRACT
 //#define DBG_EXTRACT_SUM
 
+#include "util/traits.hpp"
 #include "Literal.hpp"
 #include "constraints/Cardinality.hpp"
 #include "constraints/PseudoBoolean.hpp"
@@ -102,19 +103,22 @@ template <typename T = int> struct NumInfo {
 template <typename T = int> class NumericVar : public ExpressionFlag {
 
 public:
-    NumericVar() : ExpressionFlag(false) {}
+    constexpr NumericVar() noexcept: ExpressionFlag(false), data(Constant::NoVar, 0) {};
     NumericVar(ExpressionImpl<T> *i) : ExpressionFlag(true), implem(i) {}
     NumericVar(const var_t i, const T o = 0)
         : ExpressionFlag(false), data(i, o) {}
 
-    T min(Solver<T> &sc) const;
-    T max(Solver<T> &sc) const;
+    template<concepts::distance_provider S>
+    T min(const S &sc) const;
 
-    //    T solution_min(Solver<T> &sc) const;
-    //    T solution_max(Solver<T> &sc) const;
+    template<concepts::distance_provider S>
+    T max(const S &sc) const;
 
-    T earliest(Solver<T> &) const;
-    T latest(Solver<T> &) const;
+    template<concepts::distance_provider S>
+    T earliest(const S &) const;
+
+    template<concepts::distance_provider S>
+    T latest(const S &) const;
 
     Literal<T> after(const T t) const;
     Literal<T> before(const T t) const;
@@ -166,13 +170,13 @@ public:
 #ifdef DBG_EXTRACT
           std::cout << " extract expr" << std::endl;
 #endif
-            
+
           implem->extract(solver, target);
-            
+
 #ifdef DBG_EXTRACT
           std::cout << " ==> " << implem->id() << std::endl;
 #endif
-            
+
           solver.trash_bin.push_back(implem);
         }
 
@@ -220,7 +224,7 @@ methods
 template <typename T = int> class BooleanVar : public ExpressionFlag {
 
 public:
-  BooleanVar() { data._id_ = Constant::NoIndex; }
+  constexpr BooleanVar() noexcept: ExpressionFlag(false), data(Constant::NoIndex, Constant::NoSemantic) {}
   BooleanVar(ExpressionImpl<T> *i) : ExpressionFlag(true), implem(i) {}
   BooleanVar(const var_t i, const index_t f = 0)
       : ExpressionFlag(false), data(i, f) {}
@@ -230,7 +234,7 @@ public:
   BooleanVar<T> implies(const BooleanVar<T> x) const;
 
 var_t id() const { return (ExpressionFlag::_is_expression ? implem->id() : data._id_); }
-    
+
     index_t semantic() const { return (ExpressionFlag::_is_expression ? implem->semantic() : data._edge_id_); }
 
   operator var_t() const { return id(); }
@@ -260,11 +264,11 @@ var_t id() const { return (ExpressionFlag::_is_expression ? implem->id() : data.
 #endif
 
         implem->extract(solver, target);
-          
+
 #ifdef DBG_EXTRACT
           std::cout << " ==> " << implem->id() << std::endl;
 #endif
-          
+
         solver.trash_bin.push_back(implem);
       }
 
@@ -289,7 +293,7 @@ var_t id() const { return (ExpressionFlag::_is_expression ? implem->id() : data.
 #endif
 
     if (ExpressionFlag::_is_expression) {
-        
+
         if (implem->id() == Constant::NoVar) {
             implem->post(solver);
         }
@@ -325,7 +329,11 @@ the schedule
  */
 template <typename T = int> class Interval {
 public:
-  Interval() {}
+  constexpr Interval() noexcept : start(), end(), duration() {}
+
+  Interval(NumericVar<T> start, NumericVar<T> end, NumericVar<T> duration) :
+    start(start), end(end), duration(duration) {}
+
   Interval(Solver<T> &solver, const T mindur = 0,
            const T maxdur = Constant::Infinity<T>,
            const T earliest_start = -Constant::Infinity<T>,
@@ -366,16 +374,25 @@ public:
 
   //    Interval(const Interval<T>&) = default;
 
-  T getEarliestStart(Solver<T> &s) const;
-  T getLatestStart(Solver<T> &s) const;
-  T getEarliestEnd(Solver<T> &s) const;
-  T getLatestEnd(Solver<T> &s) const;
+  template<concepts::distance_provider S>
+  T getEarliestStart(const S &s) const;
+
+  template<concepts::distance_provider S>
+  T getLatestStart(const S &s) const;
+
+  template<concepts::distance_provider S>
+  T getEarliestEnd(const S &s) const;
+
+  template<concepts::distance_provider S>
+  T getLatestEnd(const S &s) const;
 
   bool mustExist(Solver<T> &s) const;
   bool cannotExist(Solver<T> &s) const;
 
-  T minDuration(Solver<T> &s) const;
-  T maxDuration(Solver<T> &s) const;
+  template<concepts::distance_provider S>
+  T minDuration(const S &s) const;
+  template<concepts::distance_provider S>
+  T maxDuration(const S &s) const;
 
   var_t getStart() const;
   var_t getEnd() const;
@@ -410,7 +427,6 @@ protected:
   BooleanVar<T> self{Constant::NoVar};
 };
 
-
 template <typename T = int>
 class NumericExpressionImpl : public ExpressionImpl<T> {
 public:
@@ -447,7 +463,7 @@ public:
   var_t extract(Solver<T> &solver,
                 const var_t target = Constant::NoVar) override {
 
-      
+
 #ifdef DBG_EXTRACT_SUM
       this->display(std::cout);
          std::cout << std::endl;
@@ -457,49 +473,49 @@ public:
       for(index_t i{0}; i < static_cast<index_t>(arguments.size()); ++i) {
           order.push_back(i);
       }
-      
-      
-      
-      
+
+
+
+
       std::sort(order.begin(), order.end(), [&](const index_t i, const index_t j) {return arguments[i].id() < arguments[j].id();});
-      
-      
+
+
       std::vector<NumericVar<T>> args;
     std::vector<var_t> vars;
     std::vector<T> ws;
-      
-      
+
+
       // new
       for (index_t i{0}; i < static_cast<index_t>(arguments.size()); ++i) {
           args.push_back(arguments[order[i]]);
           ws.push_back(weights[order[i]]);
           args.back().extract(solver);
-          
+
 //#ifdef DBG_EXTRACT_SUM
 //          std::cout << ws.back() << "*[" << args.back() << "]: => increase the bias by " << (ws.back() * args.back().offset()) << std::endl;
 //#endif
-//          
+//
 ////          increaseBias(ws.back() * args.back().offset());
       }
-      
+
 #ifdef DBG_EXTRACT_SUM
       arguments = args;
       weights = ws;
       this->display(std::cout);
          std::cout << std::endl;
 #endif
-      
+
       T lb{0};
       T ub{0};
       for(index_t i{static_cast<index_t>(args.size())}; i-->0;) {
           bool rm{false};
           auto x{args[i]};
           auto w{ws[i]};
-          
+
 #ifdef DBG_EXTRACT_SUM
           std::cout << w << "*[" << x << "]" ;
 #endif
-          
+
           if (w == 0) {
               rm = true;
           } else if(i>0 and x.id() == args[i-1].id()) {
@@ -512,13 +528,13 @@ public:
               increaseBias(w * x.min(solver));
               rm = true;
           }
-          
+
 #ifdef DBG_EXTRACT_SUM
           std::cout << std::endl;
 #endif
-          
+
           if(rm) {
-              
+
 #ifdef DBG_EXTRACT_SUM
               std::cout << " rm term " << w << "*" << x << std::endl;
 #endif
@@ -526,17 +542,17 @@ public:
               ws[i] = ws.back();
               args.pop_back();
               ws.pop_back();
-          } 
+          }
 #ifdef DBG_EXTRACT_SUM
           else {
               std::cout << " keep arg " << args[i] << std::endl;
           }
 #endif
-          
+
 //          increaseBias(ws[i] * args[i].offset());
-          
+
       }
-      
+
 #ifdef DBG_EXTRACT_SUM
       std::cout << args.size() << " arguments remaining\n";
       arguments = args;
@@ -544,11 +560,11 @@ public:
       this->display(std::cout);
          std::cout << std::endl;
 #endif
-      
+
       for (index_t i{0}; i < static_cast<index_t>(args.size()); ++i) {
           auto x{args[i]};
           auto w{ws[i]};
-          
+
           vars.push_back(x.id());
           if (w < 0) {
               if (lb != -Constant::Infinity<T>) {
@@ -585,8 +601,8 @@ public:
           }
       }
 
-      
-      
+
+
 //      // old
 //    T lb{0};
 //    T ub{0};
@@ -605,9 +621,9 @@ public:
 //        ws.push_back(w);
 //      }
 //    }
-//      
-//      
-//      
+//
+//
+//
 //      for (unsigned i{0}; i < arguments.size(); ++i) {
 //          auto x{arguments[i]};
 //          auto w{weights[i]};
@@ -683,19 +699,19 @@ public:
   }
 
   SumExpressionImpl<T> &addTerm(const NumericVar<T> &x, const T w = 1) {
-      
+
 //    std::cout << "\nhi ";
 //    this->display(std::cout);
 //    std::cout << std::endl;
 //    std::cout << "add term " << w << "*" << x << std::endl;
-      
+
       increaseBias(w * x.offset());
       if (x.id() != Constant::K) {
           arguments.push_back(x);
           weights.push_back(w);
       }
-          
-//          
+
+//
 //    auto idx{x.id()};
 //    if (idx == Constant::K) {
 //      //        NumericExpressionImpl<T>::self.setOffset(NumericExpressionImpl<T>::self.offset
@@ -726,7 +742,7 @@ public:
 //    std::cout << std::endl;
     return *this;
   }
-    
+
   SumExpressionImpl<T> &addTerm(const T k) {
     //    NumericExpressionImpl<T>::self.setOffset(
     //        NumericExpressionImpl<T>::self.offset() + k);
@@ -1042,7 +1058,7 @@ public:
       x.extract(solver);
       L.push_back(x == false);
     }
-      
+
       auto y = solver.newBoolean();
     L.push_back(y == true);
       
@@ -1300,7 +1316,7 @@ public:
           if (solver.getOptions().edge_finding)
               solver.postEdgeFinding(schedule, this->begin(), this->end(),
                                      this->begDisjunct());
-          
+
           if (solver.getOptions().transitivity)
               solver.postTransitivity(schedule, this->begin(), this->end(),
                                       this->begDisjunct());
@@ -1366,60 +1382,32 @@ template <typename T> NoOverlapExpression<T> NoOverlap(Interval<T> &schedule) {
  NumericVar  impl
 */
 template<typename T>
-T NumericVar<T>::min(Solver<T>& s) const {
+template<concepts::distance_provider S>
+T NumericVar<T>::min(const S& s) const {
   T v = s.numeric.lower(id());
-  //  } else {
-  //    v = -s.numeric.upper(id());
-  //  }
   if (v == -Constant::Infinity<T>)
     return v;
   return v + offset();
-  //    } else {
-  //        auto v{s.numeric.upper(id())};
-  //        if (v == Constant::Infinity<T>)
-  //            return -v;
-  //        return -v + offset();
-  //    }
 }
 
 template<typename T>
-T NumericVar<T>::max(Solver<T>& s) const {
+template<concepts::distance_provider S>
+T NumericVar<T>::max(const S& s) const {
   T v = s.numeric.upper(id());
-  //  } else {
-  //    v = -s.numeric.lower(id());
-  //  }
   if (v == Constant::Infinity<T>)
     return v;
   return v + offset();
-  //    } else {
-  //        auto v{-s.numeric.lower(id())};
-  //        if (v == Constant::Infinity<T>)
-  //            return v;
-  //        return v + offset();
-  //    }
 }
-//
-// template<typename T>
-// T NumericVar<T>::solutionMin(Solver<T>& s) const {
-//  auto v{s.numeric.lower(id())};
-//  if (v == -Constant::Infinity<T>)
-//    return v;
-//  return v + offset();
-//}
-//
-// template<typename T>
-// T NumericVar<T>::solutionMax(Solver<T>& s) const {
-//  auto v{s.numeric.upper(*this)};
-//  if (v == Constant::Infinity<T>)
-//    return v;
-//  return v + offset();
-//}
-
-template <typename T> T NumericVar<T>::earliest(Solver<T> &s) const {
+
+template <typename T>
+template<concepts::distance_provider S>
+T NumericVar<T>::earliest(const S &s) const {
   return min(s);
 }
 
-template <typename T> T NumericVar<T>::latest(Solver<T> &s) const {
+template <typename T>
+template<concepts::distance_provider S>
+T NumericVar<T>::latest(const S &s) const {
   return max(s);
 }
 
@@ -1638,19 +1626,27 @@ template <typename T> bool Interval<T>::operator==(const Interval<T> &t) const {
   return id() == t.id();
 }
 
-template <typename T> T Interval<T>::getEarliestStart(Solver<T> &solver) const {
+template <typename T>
+template<concepts::distance_provider S>
+T Interval<T>::getEarliestStart(const S &solver) const {
   return start.earliest(solver);
 }
 
-template <typename T> T Interval<T>::getLatestStart(Solver<T> &solver) const {
+template <typename T>
+template<concepts::distance_provider S>
+T Interval<T>::getLatestStart(const S &solver) const {
   return start.latest(solver);
 }
 
-template <typename T> T Interval<T>::getEarliestEnd(Solver<T> &solver) const {
+template <typename T>
+template<concepts::distance_provider S>
+T Interval<T>::getEarliestEnd(const S &solver) const {
   return end.earliest(solver);
 }
 
-template <typename T> T Interval<T>::getLatestEnd(Solver<T> &solver) const {
+template <typename T>
+template<concepts::distance_provider S>
+T Interval<T>::getLatestEnd(const S &solver) const {
   return end.latest(solver);
 }
 
@@ -1662,11 +1658,15 @@ template <typename T> bool Interval<T>::cannotExist(Solver<T> &) const {
   return false;
 }
 
-template <typename T> T Interval<T>::minDuration(Solver<T> &solver) const {
+template <typename T>
+template<concepts::distance_provider S>
+T Interval<T>::minDuration(const S &solver) const {
   return duration.min(solver);
 }
 
-template <typename T> T Interval<T>::maxDuration(Solver<T> &solver) const {
+template <typename T>
+template<concepts::distance_provider S>
+T Interval<T>::maxDuration(const S &solver) const {
   return duration.max(solver);
 }
 
diff --git a/src/header/Objective.hpp b/src/header/Objective.hpp
index f1eb825c658a145de58226472f824ecaa1a73ab6..4f369b4a345e8b81b11a14f039d81a4d215361fb 100755
--- a/src/header/Objective.hpp
+++ b/src/header/Objective.hpp
@@ -21,6 +21,8 @@
 #ifndef _TEMPO_OBJECTIVE_HPP
 #define _TEMPO_OBJECTIVE_HPP
 
+#include "Model.hpp"
+
 namespace tempo {
 
 template <typename T> class Solver;
diff --git a/src/header/Solver.hpp b/src/header/Solver.hpp
index 9dbe334da06801bbfea11eccf1b795be401efad5..f8740020f4406177911b4b1567cd3ecd2cfd33e1 100644
--- a/src/header/Solver.hpp
+++ b/src/header/Solver.hpp
@@ -41,8 +41,7 @@
 #include "constraints/OptionalEdgeConstraint.hpp"
 #include "constraints/PseudoBoolean.hpp"
 #include "constraints/Transitivity.hpp"
-#include "heuristics/HeuristicManager.hpp"
-#include "heuristics/ValueHeuristicsManager.hpp"
+#include "heuristics/heuristic_factories.hpp"
 #include "heuristics/impl/DecayingEventActivityMap.hpp"
 #include "util/KillHandler.hpp"
 #include "util/Options.hpp"
@@ -505,6 +504,9 @@ public:
 
     // must be called before the first call to 'search()'
     void initializeSearch();
+
+    template<heuristics::heuristic<T> H>
+    void setBranchingHeuristic(H &&h);
     // record the backtrack-environment level when calling 'initializeSearch()'
     int init_level{0};
     //@}
@@ -639,9 +641,7 @@ public:
      * @name search strategies
      */
     //@{
-    std::optional<heuristics::HeuristicManager<T>> heuristic;
-    std::optional<heuristics::ValueHeuristicsManager> valueHeuristic;
-
+    heuristics::PolymorphicHeuristic<T> heuristic;
     RestartManager<Solver<T>> restartPolicy;
     // @}
 
@@ -685,14 +685,13 @@ public:
     void printTrace() const;
 #endif
 
-    // to make the activity map public (to heuristics)
-    heuristics::impl::EventActivityMap<T> *activityMap{NULL};
-    
+    heuristics::impl::EventActivityMap *activityMap{nullptr};
+
 public:
-    void setActivityMap(heuristics::impl::EventActivityMap<T> *map) {
+    void setActivityMap(heuristics::impl::EventActivityMap *map) {
         activityMap = map;
     }
-    heuristics::impl::EventActivityMap<T> *getActivityMap() {
+    heuristics::impl::EventActivityMap *getActivityMap() {
         return activityMap;
     }
 
@@ -2161,14 +2160,21 @@ template <typename T> void Solver<T>::branchRight() {
 template <typename T> void Solver<T>::initializeSearch() {
     if(not initialized) {
         start_time = cpu_time();
-        heuristic.emplace(*this, options);
-        valueHeuristic.emplace(*this);
         post(&clauses);
         initialized = true;
         if(options.verbosity >= Options::QUIET)
             displayHeader(std::cout);
     }
     restartPolicy.initialize();
+    if (not heuristic.isValid()) {
+        heuristic = heuristics::make_heuristic(*this);
+    }
+}
+
+template<typename T>
+template<heuristics::heuristic<T> H>
+void Solver<T>::setBranchingHeuristic(H &&h) {
+    this->heuristic = std::forward<H>(h);
 }
 
 template <typename T> boolean_state Solver<T>::satisfiable() {
@@ -2337,8 +2343,7 @@ template <typename T> boolean_state Solver<T>::search() {
         ++num_choicepoints;
         ChoicePoint.trigger();
 
-        auto varSelection = heuristic->nextVariable(*this);
-        Literal<T> d = valueHeuristic->valueDecision(varSelection, *this);
+        Literal<T> d = heuristic.branch(*this);
         decisions.push_back(d);
 
 #ifdef DBG_TRACE
@@ -2665,7 +2670,7 @@ void Solver<T>::wake_me_on(const Literal<T> l, const int c) {
       else
           std::cout << "hi\n";
   }
-    
+
 //    std::cout <<
 }
 
diff --git a/src/header/constraints/Cardinality.hpp b/src/header/constraints/Cardinality.hpp
index d80ffedb1bc581de8e8b6b7ef62ff72b35d34d11..416e42058eeb591e199cb64f6dc879627e047c96 100644
--- a/src/header/constraints/Cardinality.hpp
+++ b/src/header/constraints/Cardinality.hpp
@@ -24,13 +24,15 @@
 #include <cassert>
 #include <vector>
 
-#include "Solver.hpp"
 #include "constraints/Constraint.hpp"
+#include "ReversibleObject.hpp"
 
 //#define DBG_LTRANS
 
 namespace tempo {
 
+template<typename T>
+class Solver;
 
 
 // enforce sum(l_i) <= bound
diff --git a/src/header/constraints/DisjunctiveEdgeFinding.hpp b/src/header/constraints/DisjunctiveEdgeFinding.hpp
index 5122451781a42c0898de74a64d2522fe03aded8b..48ad3b33244dbe476392a24c8a3bb6700f6c6f18 100644
--- a/src/header/constraints/DisjunctiveEdgeFinding.hpp
+++ b/src/header/constraints/DisjunctiveEdgeFinding.hpp
@@ -27,14 +27,17 @@
 
 #include "Explanation.hpp"
 #include "Global.hpp"
-#include "Solver.hpp"
 #include "constraints/Constraint.hpp"
 #include "util/SparseSet.hpp"
 #include "util/ThetaTree.hpp"
+#include "Model.hpp"
 
 
 namespace tempo {
 
+template<typename T>
+class Solver;
+
 template <typename T> class DisjunctiveEdgeFinding : public Constraint<T> {
 private:
   Solver<T> &m_solver;
diff --git a/src/header/constraints/PseudoBoolean.hpp b/src/header/constraints/PseudoBoolean.hpp
index 14013a1ad26b0f81a1d812d1e40033e49479fc11..c545a79efa438d2992d036641a99c27f306623d0 100644
--- a/src/header/constraints/PseudoBoolean.hpp
+++ b/src/header/constraints/PseudoBoolean.hpp
@@ -24,7 +24,6 @@
 #include <cassert>
 #include <vector>
 
-#include "Solver.hpp"
 #include "util/SparseSet.hpp"
 #include "constraints/Constraint.hpp"
 
@@ -32,6 +31,9 @@
 
 namespace tempo {
 
+template<typename T>
+class Solver;
+
 
 // enforce sum(l_i) <= bound
 template <typename T> class PseudoBoolean : public Constraint<T> {
diff --git a/src/header/constraints/SumConstraint.hpp b/src/header/constraints/SumConstraint.hpp
index eea7da929015d4295db5d5f497dae877ca573e8b..07c0f5a93d469c33a21806a36707ddc4f1c56767 100644
--- a/src/header/constraints/SumConstraint.hpp
+++ b/src/header/constraints/SumConstraint.hpp
@@ -24,7 +24,6 @@
 #include <cassert>
 #include <vector>
 
-#include "Solver.hpp"
 #include "constraints/Constraint.hpp"
 #include "util/SparseSet.hpp"
 
@@ -32,6 +31,9 @@
 
 namespace tempo {
 
+template<typename T>
+class Solver;
+
 // enforce sum(w_i * x_i) <= bound
 template <typename T> class SumConstraint : public Constraint<T> {
 private:
diff --git a/src/header/constraints/Transitivity.hpp b/src/header/constraints/Transitivity.hpp
index 5eac38fe85fd1a713b5073d50e68e507c9c813fe..dc5ceb8bd6b661a055c1e0fd443b6daa46186085 100644
--- a/src/header/constraints/Transitivity.hpp
+++ b/src/header/constraints/Transitivity.hpp
@@ -28,15 +28,18 @@
 #include "Explanation.hpp"
 #include "Global.hpp"
 #include "ReversibleObject.hpp"
-#include "Solver.hpp"
 #include "constraints/Constraint.hpp"
 #include "util/DisjointSet.hpp"
 #include "util/SparseSet.hpp"
+#include "Model.hpp"
 
 //#define DBG_LTRANS
 
 namespace tempo {
 
+template<typename T>
+class Solver;
+
 template <typename T> class Transitivity : public Constraint<T> {
 private:
   Solver<T> &m_solver;
diff --git a/src/header/heuristics/BaseBooleanHeuristic.hpp b/src/header/heuristics/BaseBooleanHeuristic.hpp
index b5ad06f6c339a1478ef6b3359663e1e6c21c80c3..9721fd688e664b3e83c35fa3adba88855238c542 100644
--- a/src/header/heuristics/BaseBooleanHeuristic.hpp
+++ b/src/header/heuristics/BaseBooleanHeuristic.hpp
@@ -23,22 +23,16 @@
 
 #include <concepts>
 #include <stdexcept>
+#include <cassert>
 
 #include "Global.hpp"
 #include "Literal.hpp"
 #include "util/crtp.hpp"
 #include "util/traits.hpp"
+#include "heuristics/heuristic_interface.hpp"
 
 namespace tempo::heuristics {
 
-/**
- * @brief Contains all information necessary for instantiating value selection
- * heuristics
- */
-struct ValueHeuristicConfig {
-    double epsilon;
-};
-
 /**
  * Interface for value selection heuristic implementations that derive from
  * BaseBooleanHeuristic
@@ -52,16 +46,6 @@ concept binary_heuristic_implementation = requires(H heuristic,
   { heuristic.choose(x, solver) } -> concepts::same_template<Literal>;
 };
 
-/**
- * Interface for value selection heuristics
- * @tparam H heuristic type
- * @tparam Solver information provider (usually the scheduler)
- */
-template <typename H, typename Solver>
-concept value_heuristic = requires(H heuristic, VariableSelection x, const Solver &solver) {
-    { heuristic.valueDecision(x, solver) } -> concepts::same_template<Literal>;
-};
-
 template<typename Solver>
 concept boolean_info_provider = requires(const Solver s, var_t x) {
     { s.boolean.getLiteral(true, x) } -> concepts::same_template<Literal>;
diff --git a/src/header/heuristics/GNNValueHeuristics.hpp b/src/header/heuristics/GNNValueHeuristics.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c63467adb224a8ea96986acd5e4c87c80e30dad3
--- /dev/null
+++ b/src/header/heuristics/GNNValueHeuristics.hpp
@@ -0,0 +1,66 @@
+/**
+* @author Tim Luchterhand
+* @date 08.07.24
+* @brief
+*/
+
+#ifndef TEMPO_GNNVALUEHEURISTICS_HPP
+#define TEMPO_GNNVALUEHEURISTICS_HPP
+
+#include <filesystem>
+
+#include "util/traits.hpp"
+#include "Global.hpp"
+#include "nn/GNNEdgePolarityPredictor.hpp"
+#include "BaseBooleanHeuristic.hpp"
+#include "util/SchedulingProblemHelper.hpp"
+
+namespace fs = std::filesystem;
+
+namespace tempo {
+    template<typename T>
+    class Solver;
+}
+
+namespace tempo::heuristics {
+
+    /**
+     * @brief Full guidance GNN based value branching heuristic.
+     * @detail @copybrief
+     * The GNN is called at every decision.
+     * @tparam T timing type
+     * @tparam R resource type
+     * @note This Value branching heuristic is only ment to be used for scheduling problems
+     */
+    template<concepts::scalar T, SchedulingResource R>
+    class GNNFullGuidance: public BaseBooleanHeuristic<GNNFullGuidance<T, R>> {
+        nn::heuristics::GNNEdgePolarityPredictor<T, R> polarityPredictor;
+    public:
+        /**
+         * Ctor
+         * @param epsilon epsilon greedy value (see BaseBooleanHeuristic)
+         * @param modelLocation location of the trained GNN model
+         * @param featureExtractorConfigLocation location of the feature extractor configuration
+         * (tempo::nn:GraphBuilderConfig)
+         * @param problem initial description of the problem
+         */
+        GNNFullGuidance(double epsilon, const fs::path &modelLocation, const fs::path &featureExtractorConfigLocation,
+                        SchedulingProblemHelper <T, R> problem) : BaseBooleanHeuristic<GNNFullGuidance<T, R>>(epsilon),
+                                                                  polarityPredictor(modelLocation,
+                                                                                    featureExtractorConfigLocation,
+                                                                                    std::move(problem)) {}
+
+        /**
+         * BaseBooleanHeuristic interface
+         * @param x variable id
+         * @param solver solver for which to generate literal
+         * @returns branching Literal
+         */
+        auto choose(var_t x, const Solver<T> &solver) -> Literal<T> {
+            polarityPredictor.preEvaluation(solver);
+            return polarityPredictor.choose(x, solver);
+        }
+    };
+}
+
+#endif //TEMPO_GNNVALUEHEURISTICS_HPP
diff --git a/src/header/heuristics/HeuristicManager.hpp b/src/header/heuristics/HeuristicManager.hpp
deleted file mode 100644
index c3fc8a73c48770c01a037d63f4ac05f02e2ad3b6..0000000000000000000000000000000000000000
--- a/src/header/heuristics/HeuristicManager.hpp
+++ /dev/null
@@ -1,99 +0,0 @@
-/************************************************
- * Tempo HeuristicManager.hpp
- *
- * Copyright 2024 Tim Luchterhand  and Emmanuel Hebrard
- *
- * Tempo is free software: you can redistribute it and/or modify it
- * under the terms of the GNU General Public License as published by the
- * Free Software Foundation, either version 3 of the License, or (at your
- *  option) any later version.
- *
- * Tempo is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Tempo.  If not, see <http://www.gnu.org/licenses/>.
- *
- ***********************************************/
-
-#ifndef TEMPO_HEURISTICMANAGER_HPP
-#define TEMPO_HEURISTICMANAGER_HPP
-
-#include <variant>
-#include <exception>
-#include <utility>
-
-#include "VSIDS.hpp"
-#include "Tightest.hpp"
-#include "WeightedDegree.hpp"
-
-
-namespace tempo {
-    template<typename T>
-    class Solver;
-}
-
-/**
- * @brief namespace containing variable (choice point) selection heuristics
- */
-namespace tempo::heuristics {
-    /**
-     * @brief Heuristic factory class that can be used to construct different heuristics and at the same time provides a
-     * consistent interface to callers
-     */
-    template<concepts::scalar T>
-    class HeuristicManager {
-        using Implementations = std::variant<Tightest, VSIDS<T>, WeightedDegree<T>>;
-
-    public:
-        /**
-         * Ctor: Internally constructs the heuristic inferred from the given
-         * arguments
-         * @param solver solver for which to create a heuristic
-         * @param options options specifying the type of heuristic and further
-         * config values
-         * @throws std::runtime_error if an unknown heuristics type was given in
-         * options
-         */
-        HeuristicManager(Solver<T> &solver, const Options &options) {
-            //@TODO use factory pattern
-            switch (options.choice_point_heuristics) {
-                case Options::ChoicePointHeuristics::Tightest: {
-                    impl.template emplace<Tightest>();
-                    break;
-                }
-                case Options::ChoicePointHeuristics::VSIDS: {
-                    if (options.learning) {
-                        impl.template emplace<VSIDS<T>>(solver);
-                    } else // closest thing if not learning
-                        impl.template emplace<WeightedDegree<T>>(solver);
-                    break;
-                }
-                case Options::ChoicePointHeuristics::WeightedDegree: {
-                    impl.template emplace<WeightedDegree<T>>(solver);
-                    break;
-                }
-                default:
-                    throw std::runtime_error("unknown heuristic type");
-            }
-        }
-
-
-        /**
-         * Calls the internally stored heuristic with the given arguments
-         * @param solver solver for which to select the next variable
-         * @return variable choice consisting of the selected variable and its type
-         */
-        auto nextVariable(Solver<T> &solver) {
-            return std::visit([&solver](auto &heuristic) { return heuristic.nextVariable(solver); }, impl);
-        }
-
-    private:
-        Implementations impl{};
-    };
-}
-
-#endif //TEMPO_HEURISTICMANAGER_HPP
-
diff --git a/src/header/heuristics/RandomBinaryValue.hpp b/src/header/heuristics/RandomBinaryValue.hpp
index bfc799d12978b222b37e6cda1d898302a3452e2f..77b1486d403fe35436cb1d31094c5e161bc3313e 100644
--- a/src/header/heuristics/RandomBinaryValue.hpp
+++ b/src/header/heuristics/RandomBinaryValue.hpp
@@ -25,7 +25,6 @@
 
 #include "BaseBooleanHeuristic.hpp"
 #include "Global.hpp"
-#include "util/factory_pattern.hpp"
 
 namespace tempo::heuristics {
 
@@ -51,8 +50,6 @@ namespace tempo::heuristics {
         }
     };
 
-    MAKE_DEFAULT_FACTORY(RandomBinaryValue, const ValueHeuristicConfig &)
-
 } // namespace tempo::heuristics
 
 #endif // TEMPO_RANDOMBINARYVALUE_HPP
diff --git a/src/header/heuristics/RankingHeuristic.hpp b/src/header/heuristics/RankingHeuristic.hpp
index 7f5992054cc76f16b722dc42c22d3c9e7ce652d5..e91019d3154809aba597e9f29a5c1aeac0c8b1d7 100755
--- a/src/header/heuristics/RankingHeuristic.hpp
+++ b/src/header/heuristics/RankingHeuristic.hpp
@@ -24,6 +24,7 @@
 
 #include <concepts>
 
+#include "heuristic_interface.hpp"
 #include "util/crtp.hpp"
 #include "Constant.hpp"
 
diff --git a/src/header/heuristics/Tightest.hpp b/src/header/heuristics/Tightest.hpp
index ef37e47ae75dd061488571476b1bd192200a06d0..96a57b1fb8ff2f439c756cc9c19dddcbb2415e75 100755
--- a/src/header/heuristics/Tightest.hpp
+++ b/src/header/heuristics/Tightest.hpp
@@ -12,6 +12,8 @@ namespace tempo::heuristics {
 
     class Tightest : public RankingHeuristic<Tightest> {
     public:
+        Tightest() = default;
+
         /**
          * Calculates the cost for a boolean variable which is the maximum of the distance
          * between the nodes in both directions of the associated distance constraint
diff --git a/src/header/heuristics/TightestValue.hpp b/src/header/heuristics/TightestValue.hpp
index 966830dfbc2169d1e7c485cf04b6bba4f4450cd1..6e59073ca4499637b14b8cd007551a5fbc40d166 100644
--- a/src/header/heuristics/TightestValue.hpp
+++ b/src/header/heuristics/TightestValue.hpp
@@ -12,7 +12,6 @@
 #include "DistanceConstraint.hpp"
 #include "Global.hpp"
 #include "Literal.hpp"
-#include "util/factory_pattern.hpp"
 #include "util/traits.hpp"
 
 
@@ -20,10 +19,8 @@ namespace tempo::heuristics {
 
 namespace detail {
 template <typename Solver>
-concept distance_provider = requires(const Solver s, var_t x, var_t e) {
+concept edge_distance_provider = concepts::distance_provider<Solver> and requires(const Solver s, var_t x) {
   { s.boolean.getEdge(true, x) } -> concepts::same_template<DistanceConstraint>;
-  { s.numeric.upper(e) } -> concepts::scalar;
-  { s.numeric.lower(e) } -> concepts::scalar;
 };
 }
 
@@ -49,7 +46,7 @@ public:
    * @param solver scheduler instance
    * @return either POS(cp) or NEG(cp)
    */
-  template <detail::distance_provider Solver>
+  template <detail::edge_distance_provider Solver>
   requires(boolean_info_provider<Solver>) static auto choose(
       var_t x, const Solver &solver) {
     // @TODO no gap info available -> what should I return?
@@ -66,11 +63,6 @@ public:
     return solver.boolean.getLiteral(gapPos >= gapNeg, x);
   }
 };
-
-MAKE_FACTORY(TightestValue, const ValueHeuristicConfig &config) {
-  return TightestValue(config.epsilon);
-}
-};
 }
 
 #endif // TEMPO_TIGHTESTVALUE_HPP
diff --git a/src/header/heuristics/VSIDS.hpp b/src/header/heuristics/VSIDS.hpp
index 1a94c2d1d5173969def2903bf267fa6720a7a04a..88c0ab5fb9fc45b5ca7b8f58c73627378ba7a372 100644
--- a/src/header/heuristics/VSIDS.hpp
+++ b/src/header/heuristics/VSIDS.hpp
@@ -20,8 +20,7 @@ namespace tempo::heuristics {
      * the the number of calls to the function VSIDS::updateActivity
      * The final score of a literal is inversely proportional to its activity (@see getCost)
      */
-    template<typename T>
-    class VSIDS : public RankingHeuristic<VSIDS<T>> {
+    class VSIDS : public RankingHeuristic<VSIDS> {
     public:
 
 
@@ -29,6 +28,7 @@ namespace tempo::heuristics {
          * CTor. Infers parameters from given scheduler. Subscribes to events of interest
          * @param solver target solver
          */
+        template<concepts::scalar T>
         explicit VSIDS(Solver<T> &solver) :
                 activity(solver, solver.getOptions().vsids_decay),
                 handlerToken(solver.ClauseAdded.subscribe_handled(
@@ -45,6 +45,7 @@ namespace tempo::heuristics {
 
         ~VSIDS() = default;
 
+        template<concepts::scalar T>
         [[nodiscard]] double getCost(const var_t x, const Solver<T> &solver) const {
 
             //@TODO: there shoud be a normalization thingy and Boolean variables without semantic should get the highest value
@@ -74,12 +75,13 @@ namespace tempo::heuristics {
          * @param solver
          * @todo currently only selects boolean variables
          */
+        template<concepts::scalar T>
         auto nextVariable(const Solver<T> &solver) const -> VariableSelection {
             return {this->bestVariable(solver.getBranch(), solver), VariableType::Boolean};
         }
 
     private:
-        impl::DecayingEventActivityMap<T> activity;
+        impl::DecayingEventActivityMap activity;
         SubscriberHandle handlerToken;
     };
 }
diff --git a/src/header/heuristics/ValueHeuristicsManager.hpp b/src/header/heuristics/ValueHeuristicsManager.hpp
deleted file mode 100644
index f543d851d39d05a6cf581f98195286570da03ad8..0000000000000000000000000000000000000000
--- a/src/header/heuristics/ValueHeuristicsManager.hpp
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * @author Tim Luchterhand
- * @date 06.05.24
- * @brief
- */
-
-#ifndef TEMPO_VALUEHEURISTICSMANAGER_HPP
-#define TEMPO_VALUEHEURISTICSMANAGER_HPP
-#include <optional>
-#include <string>
-
-#include "RandomBinaryValue.hpp"
-//#include "SolutionGuided.hpp"
-#include "TightestValue.hpp"
-#include "util/Options.hpp"
-#include "util/factory_pattern.hpp"
-#include "util/traits.hpp"
-
-namespace tempo {
-template <typename T> class Solver;
-}
-
-namespace tempo::heuristics {
-
-auto valHeuristicTypeToString(Options::PolarityHeuristic type) -> std::string;
-
-MAKE_FACTORY_PATTERN(ValueHeuristic, ValueHeuristicConfig, TightestValue,
-                     /*SolutionGuided<TightestValue>,*/ RandomBinaryValue)
-
-class ValueHeuristicsManager {
-  std::optional<ValueHeuristic> impl;
-  template <typename T> struct FactoryChecker {
-    HOLDS_FOR_ALL(ValueHeuristic, value_heuristic, T)
-  };
-
-public:
-  template <concepts::scalar T>
-  explicit ValueHeuristicsManager(const Solver<T> &scheduler) {
-    impl.emplace(ValueHeuristicFactory::getInstance().create(
-        valHeuristicTypeToString(scheduler.getOptions().polarity_heuristic),
-        ValueHeuristicConfig{.epsilon =
-                                 scheduler.getOptions().polarity_epsilon}));
-  }
-
-  template <typename Sched>
-  auto valueDecision(const VariableSelection &selection, const Sched &scheduler) {
-    static_assert(FactoryChecker<Sched>::template __ValueHeuristic_tester__<
-                      ValueHeuristic>::value,
-                  "At least one heuristic has an invalid signature");
-    return std::visit([&](auto &h) { return h.valueDecision(selection, scheduler); }, *impl);
-  }
-};
-
-} // namespace tempo::heuristics
-
-#endif // TEMPO_VALUEHEURISTICSMANAGER_HPP
diff --git a/src/header/heuristics/WeightedDegree.hpp b/src/header/heuristics/WeightedDegree.hpp
index 60e9f7f32d1f59df95673429df8cc8a51d801959..ed46e8cc3e3b94c44731f1618d33c1acbcf10a53 100755
--- a/src/header/heuristics/WeightedDegree.hpp
+++ b/src/header/heuristics/WeightedDegree.hpp
@@ -22,17 +22,17 @@ namespace tempo::heuristics {
      * how often it is involved in a conflict and not how often it is contained in a
      * learned clause
      */
-    template<typename T>
-    class WeightedDegree : public RankingHeuristic<WeightedDegree<T>> {
+    class WeightedDegree : public RankingHeuristic<WeightedDegree> {
     public:
 
+        template<concepts::scalar T>
         explicit WeightedDegree(Solver<T> &solver)
                 : activity(solver, solver.getOptions().vsids_decay),
                   handlerToken(solver.ConflictEncountered.subscribe_handled(
-                          [this, &solver](auto &expl) {
-                              this->clause.clear();
-                              expl.explain(Solver<T>::Contradiction, this->clause);
-                              this->activity.update(this->clause, solver);
+                          [this, &solver, clause=std::vector<Literal<T>>{}](auto &expl) mutable {
+                              clause.clear();
+                              expl.explain(Solver<T>::Contradiction, clause);
+                              this->activity.update(clause, solver);
                           })) {}
 
         WeightedDegree(const WeightedDegree &) = delete;
@@ -42,6 +42,7 @@ namespace tempo::heuristics {
         ~WeightedDegree() = default;
 
 
+        template<concepts::scalar T>
         [[nodiscard]] double getCost(const var_t x, const Solver<T> &solver) const {
             double dom{1};
 
@@ -65,14 +66,14 @@ namespace tempo::heuristics {
          * @param solver
          * @todo currently only selects boolean variables
          */
+        template<concepts::scalar T>
         auto nextVariable(const Solver<T> &solver) const -> VariableSelection {
             return {this->bestVariable(solver.getBranch(), solver), VariableType::Boolean};
         }
 
 private:
-  impl::DecayingEventActivityMap<T> activity;
+  impl::DecayingEventActivityMap activity;
   SubscriberHandle handlerToken;
-  std::vector<Literal<T>> clause;
 };
 }
 
diff --git a/src/header/heuristics/heuristic_factories.hpp b/src/header/heuristics/heuristic_factories.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c3d49dcfc4cb6d685bcf7a14d418aaf4fbb924c1
--- /dev/null
+++ b/src/header/heuristics/heuristic_factories.hpp
@@ -0,0 +1,146 @@
+/**
+* @author Tim Luchterhand
+* @date 09.07.24
+* @brief
+*/
+
+#ifndef TEMPO_HEURISTIC_FACTORIES_HPP
+#define TEMPO_HEURISTIC_FACTORIES_HPP
+
+#include <nlohmann/json.hpp>
+#include <string>
+
+#include "heuristic_interface.hpp"
+#include "Tightest.hpp"
+#include "VSIDS.hpp"
+#include "WeightedDegree.hpp"
+#include "RandomBinaryValue.hpp"
+#include "TightestValue.hpp"
+
+#include "util/factory_pattern.hpp"
+#include "util/Options.hpp"
+#include "util/traits.hpp"
+
+namespace tempo {
+    template<typename T>
+    class Solver;
+}
+
+namespace tempo::heuristics {
+
+    /// --- Variable Heuristics ---
+
+    namespace detail {
+        using VSIDS_M = MovableHeuristic<VSIDS>;
+        using WeightedDegree_M = MovableHeuristic<WeightedDegree>;
+
+        auto getVarHName(Options::ChoicePointHeuristics heuristic) -> std::string;
+        auto getValHName(Options::PolarityHeuristic heuristic) -> std::string;
+
+        template<typename ...Heuristics>
+        struct VariantHeuristicWrapper : std::variant<Heuristics...> {
+            using std::variant<Heuristics...>::variant;
+            using std::variant<Heuristics...>::emplace;
+
+            template<concepts::scalar T> requires(variable_heuristic<Heuristics, T> && ...)
+            auto nextVariable(const Solver<T> &solver) {
+                return std::visit([&solver](auto &h) {return h.nextVariable(solver);}, *this);
+            }
+
+            template<concepts::scalar T> requires(value_heuristic<Heuristics, Solver<T>> && ...)
+            auto valueDecision(VariableSelection x, const Solver<T> &solver) {
+                return std::visit([x, &solver](auto &h) {return h.valueDecision(x, solver);}, *this);
+            }
+
+            template<concepts::scalar T> requires(heuristic<Heuristics, T> && ...)
+            auto branch(const Solver<T> &solver) {
+                return std::visit([&solver](auto &h) {return h.branch(solver);}, *this);
+            }
+        };
+
+
+    }
+
+    using VariableHeuristic = detail::VariantHeuristicWrapper<Tightest, detail::VSIDS_M, detail::WeightedDegree_M>;
+
+    /// Define heuristic factory types here
+
+    MAKE_TEMPLATE_FACTORY(Tightest, concepts::scalar T, const Solver<T> &) {
+            return Tightest{};
+        }
+    };
+
+    MAKE_TEMPLATE_P_FACTORY(VSIDS, VariableHeuristic, concepts::scalar T, Solver<T> &solver) {
+            if(solver.getOptions().learning) {
+                return detail::VSIDS_M(solver);
+            } else {
+                return detail::WeightedDegree_M(solver);
+            }
+        }
+    };
+
+    MAKE_TEMPLATE_FACTORY(WeightedDegree, concepts::scalar T, Solver<T> &solver) {
+            return detail::WeightedDegree_M(solver);
+        }
+    };
+
+    MAKE_T_FACTORY_PATTERN(VariableHeuristic, template<concepts::scalar T>, Solver<T>&, Tightest, VSIDS, WeightedDegree)
+
+    /// --- Value Heuristics ---
+
+    /// Add further heuristic types here
+
+    using ValueHeuristic = detail::VariantHeuristicWrapper<TightestValue, RandomBinaryValue>;
+
+    /// Define heuristic factory types here
+
+    MAKE_FACTORY(TightestValue, const Options &options) {
+            return TightestValue(options.polarity_epsilon);
+        }
+    };
+
+    MAKE_DEFAULT_FACTORY(RandomBinaryValue, const Options&)
+
+    MAKE_FACTORY_PATTERN(ValueHeuristic, const Options&, TightestValue, RandomBinaryValue)
+
+
+    /// --- Use these factory methods ---
+
+    /**
+     * Infers variable selection heuristic from solver options
+     * @tparam T timing type
+     * @param solver solver for which to create variable selection heuristic
+     * @return variable selection heuristic inferred from solver options
+     */
+    template<concepts::scalar T>
+    auto make_variable_heuristic(Solver<T> &solver) {
+        const auto &options = solver.getOptions();
+        return VariableHeuristicFactory::getInstance().create(detail::getVarHName(options.choice_point_heuristics),
+                                                              solver);
+    }
+
+    /**
+     * Infers value selection heuristic from solver options
+     * @tparam T timing type
+     * @param solver solver for which to create value selection heuristic
+     * @return value selection heuristic inferred from solver options
+     */
+    template<concepts::scalar T>
+    auto make_value_heuristic(Solver<T> &solver) {
+        const auto &options = solver.getOptions();
+        return ValueHeuristicFactory::getInstance().create(detail::getValHName(options.polarity_heuristic), options);
+    }
+
+    /**
+     * Infers branching heuristics to be used from solver options
+     * @tparam T timing type
+     * @param solver for which to create branching heuristic
+     * @return branching heuristic inferred from solver options
+     */
+    template<concepts::scalar T>
+    auto make_heuristic(Solver<T> &solver) {
+        return make_compound_heuristic(make_variable_heuristic(solver), make_value_heuristic(solver));
+    }
+}
+
+#endif //TEMPO_HEURISTIC_FACTORIES_HPP
diff --git a/src/header/heuristics/heuristic_interface.hpp b/src/header/heuristics/heuristic_interface.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..7c183f9444cca71549b86684bcaa2d6816a9223c
--- /dev/null
+++ b/src/header/heuristics/heuristic_interface.hpp
@@ -0,0 +1,225 @@
+/**
+* @author Tim Luchterhand
+* @date 09.07.24
+* @brief
+*/
+
+#ifndef TEMPO_HEURISTIC_INTERFACE_HPP
+#define TEMPO_HEURISTIC_INTERFACE_HPP
+
+#include <concepts>
+#include <memory>
+#include <variant>
+#include <functional>
+
+#include "Literal.hpp"
+#include "util/traits.hpp"
+
+namespace tempo {
+    template<typename T>
+    class Solver;
+}
+
+namespace tempo::heuristics {
+    enum class VariableType {
+        Boolean,
+        Numeric
+    };
+
+    using VariableSelection = std::pair<var_t, VariableType>;
+
+    /**
+     * Interface for branching heuristics (combined variable and value selection)
+     * @tparam H heuristic type
+     * @tparam T timing type
+     */
+    template<typename H, typename T>
+    concept heuristic = requires(H heuristic, const Solver<T> solver) {
+        { heuristic.branch(solver) } -> std::same_as<Literal<T>>;
+    };
+
+    /**
+     * Interface for variable selection heuristics
+     * @tparam H heuristic type
+     * @tparam T timing type
+     */
+    template<typename H, typename T>
+    concept variable_heuristic = requires(H heuristic, const Solver<T> solver) {
+        { heuristic.nextVariable(solver) } -> std::same_as<VariableSelection>;
+    };
+
+    /**
+     * Interface for value selection heuristics
+     * @tparam H heuristic type
+     * @tparam Solver information provider (usually the scheduler)
+     */
+    template <typename H, typename Solver>
+    concept value_heuristic = requires(H heuristic, VariableSelection x, const Solver &solver) {
+        { heuristic.valueDecision(x, solver) } -> concepts::same_template<Literal>;
+    };
+
+    /**
+     * Wrapper class for heuristics that do not support moving
+     * @tparam H type of heuristic
+     */
+    template<typename H>
+    class MovableHeuristic {
+        std::unique_ptr<H> heuristic;
+    public:
+        /**
+         * Ctor. Constructs the heuristic in place
+         * @tparam Args argument types
+         * @param args arguments to the Ctor of H
+         */
+        template<typename ...Args>
+        explicit MovableHeuristic(Args &&...args) : heuristic(std::make_unique<H>(std::forward<Args>(args)...)) {}
+
+
+        /**
+         * Variable heuristic interface
+         * @tparam T timing type
+         * @param solver
+         * @return variable selection
+         */
+        template<concepts::scalar T> requires(variable_heuristic<H, T>)
+        auto nextVariable(const Solver<T> &solver) const -> VariableSelection {
+            return heuristic->nextVariable(solver);
+        }
+
+        /**
+         * Value heuristic interface
+         * @tparam T timing type
+         * @param x variable selection
+         * @param solver
+         * @return branching literal
+         */
+        template<concepts::scalar T> requires(value_heuristic<H, Solver<T>>)
+        auto valueDecision(VariableSelection x, const Solver<T> &solver) const -> Literal<T> {
+            return heuristic->valueDecision(x, solver);
+        }
+
+        /**
+         * Heuristic interface
+         * @tparam T timing type
+         * @param solver
+         * @return branching literal
+         */
+        template<concepts::scalar T> requires(tempo::heuristics::heuristic<H, T>)
+        auto branch(const Solver<T> &solver) const -> Literal<T> {
+            return heuristic->branch(solver);
+        }
+    };
+
+    namespace detail {
+        template<concepts::scalar T>
+        struct PolyHeuristicCallableBase {
+            PolyHeuristicCallableBase() = default;
+            virtual ~PolyHeuristicCallableBase() = default;
+            PolyHeuristicCallableBase(PolyHeuristicCallableBase&&) = default;
+            PolyHeuristicCallableBase &operator=(PolyHeuristicCallableBase&&) = default;
+            virtual auto invoke(const Solver<T> &) -> Literal<T> = 0;
+        };
+
+        template<concepts::scalar T, heuristic<T> H>
+        struct PolyHeuristicCallable : PolyHeuristicCallableBase<T> {
+            H impl;
+            template<typename ...Args>
+            explicit PolyHeuristicCallable(Args &&...args) : impl(std::forward<Args>(args)...) {}
+            auto invoke(const Solver<T> &solver) -> Literal<T> override {
+                return impl.branch(solver);
+            }
+        };
+    }
+
+    /**
+     * @brief Polymorphic wrapper for arbitrary branching heuristics
+     * @details @copybrief
+     * (combined variable selection + value selection)
+     * @tparam T timing type
+     * @note use std::movable_function from c++23 instead of detail::PolyHeuristicCallable implementation in the future
+     */
+    template<concepts::scalar T>
+    class PolymorphicHeuristic {
+        std::unique_ptr<detail::PolyHeuristicCallableBase<T>> impl;
+    public:
+        /**
+         * default ctor
+         * Creates an invalid wrapper that must not be called
+         */
+        constexpr PolymorphicHeuristic() noexcept: impl(nullptr) {}
+
+        /**
+         * Ctor
+         * @tparam H heuristic type
+         */
+        template<heuristic<T> H>
+        PolymorphicHeuristic(H &&heuristic) :impl(
+                std::make_unique<detail::PolyHeuristicCallable<T, std::decay_t<H>>>(std::forward<H>(heuristic))) {}
+
+        /**
+         * Branching heuristic interface
+         * @param solver solver for which to select a branching literal
+         * @return selected branching literal
+         * @throws std::runtime_error if wrapper does not contain valid heuristic
+         */
+        auto branch(const Solver<T> &solver) -> Literal<T> {
+            if (nullptr == impl) {
+                throw std::runtime_error("no branching heuristic set");
+            }
+
+            return impl->invoke(solver);
+        }
+
+        /**
+         * Whether the wrapper holds a valid heuristic
+         */
+        [[nodiscard]] bool isValid() const noexcept {
+            return nullptr != impl;
+        }
+    };
+
+    /**
+     * @brief General branching heuristic wrapper that holds a separate variable and value selection heuristic
+     * @details @copybrief
+     * @tparam VarH type of variable selection heuristic
+     * @tparam ValH type of value selection heuristic
+     */
+    template<typename VariableHeuristic, typename ValueHeuristic>
+    class CompoundHeuristic {
+        VariableHeuristic variableSelector;
+        ValueHeuristic valueSelector;
+    public:
+
+        /**
+         * Ctor
+         * @tparam VarH type of variable heuristic
+         * @tparam ValH type of value heuristic
+         * @param variableHeuristic the variable heuristic
+         * @param valueHeuristic the value heuristic
+         */
+        template<typename VarH, typename ValH>
+        CompoundHeuristic(VarH &&variableHeuristic, ValH &&valueHeuristic) :
+                variableSelector(std::forward<VarH>(variableHeuristic)),
+                valueSelector(std::forward<ValH>(valueHeuristic)) {}
+
+        template<concepts::scalar T>
+        requires(variable_heuristic<VariableHeuristic, T> and value_heuristic<ValueHeuristic, Solver<T>>)
+        auto branch(const Solver<T> &solver) -> Literal<T> {
+            return valueSelector.valueDecision(variableSelector.nextVariable(solver), solver);
+        }
+    };
+
+    /**
+     * Helper function to create compound heuristics
+     * @tparam VarH type of variable heuristic
+     * @tparam ValH type of value heuristic
+     * @param variableHeuristic the variable heuristic
+     * @param valueHeuristic the value heuristic
+     */
+    template<typename VarH, typename ValH>
+    auto make_compound_heuristic(VarH &&variableHeuristic, ValH &&valueHeuristic) {
+        return CompoundHeuristic<VarH, ValH>(std::forward<VarH>(variableHeuristic), std::forward<ValH>(valueHeuristic));
+    }
+}
+
+#endif //TEMPO_HEURISTIC_INTERFACE_HPP
diff --git a/src/header/heuristics/impl/DecayingEventActivityMap.hpp b/src/header/heuristics/impl/DecayingEventActivityMap.hpp
index 8f16b24cc33122d8fb78dec435539856ae01169a..59e3444b2b48cc0b655ce4b0cb52c4b132954865 100755
--- a/src/header/heuristics/impl/DecayingEventActivityMap.hpp
+++ b/src/header/heuristics/impl/DecayingEventActivityMap.hpp
@@ -11,8 +11,7 @@ namespace tempo::heuristics::impl {
     /**
      * @brief Records activity for literals. Activity of inactive literals decays which each update
      */
-template<typename T>
-    class DecayingEventActivityMap : public EventActivityMap<T> {
+    class DecayingEventActivityMap : public EventActivityMap {
     public:
         /**
          * @copydoc ActivityMap::ActivityMap
@@ -25,9 +24,10 @@ template<typename T>
 //                throw std::runtime_error("invalid decay value");
 //            }
 //        }
-        
+
+        template<concepts::scalar T>
         explicit DecayingEventActivityMap(Solver<T> &solver, double decay) :
-        EventActivityMap<T>(solver), decay(decay) {
+        EventActivityMap(solver), decay(decay) {
             if (decay <= 0 or decay > 1) {
                 throw std::runtime_error("invalid decay value");
             }
@@ -43,24 +43,25 @@ template<typename T>
 //            EventActivityMap<T>::numeric_activity[x] += increment;
 //            return (EventActivityMap<T>::numeric_activity[x] > maxActivity);
 //        }
-        
+
+        template<concepts::scalar T>
         bool incrementActivity(const Literal<T> l, const Solver<T>& solver) noexcept {
             
 //            std::cout << "increment activity of " << l << std::endl;
             
             bool need_rescaling{false};
             if(l.isNumeric()) {
-                EventActivityMap<T>::numeric_activity[l.variable()] += increment;
-                need_rescaling = EventActivityMap<T>::numeric_activity[l.variable()] > maxActivity;
+                EventActivityMap::numeric_activity[l.variable()] += increment;
+                need_rescaling = EventActivityMap::numeric_activity[l.variable()] > maxActivity;
             } else {
-                EventActivityMap<T>::boolean_activity[l.variable()] += increment;
-                need_rescaling = EventActivityMap<T>::boolean_activity[l.variable()] > maxActivity;
+                EventActivityMap::boolean_activity[l.variable()] += increment;
+                need_rescaling = EventActivityMap::boolean_activity[l.variable()] > maxActivity;
                 if(l.hasSemantic()) {
                     auto c{solver.boolean.getEdge(l)};
-                    EventActivityMap<T>::numeric_activity[c.from] += increment;
-                    EventActivityMap<T>::numeric_activity[c.to] += increment;
-                    need_rescaling |= EventActivityMap<T>::numeric_activity[c.from] > maxActivity;
-                    need_rescaling |= EventActivityMap<T>::numeric_activity[c.to] > maxActivity;
+                    EventActivityMap::numeric_activity[c.from] += increment;
+                    EventActivityMap::numeric_activity[c.to] += increment;
+                    need_rescaling |= EventActivityMap::numeric_activity[c.from] > maxActivity;
+                    need_rescaling |= EventActivityMap::numeric_activity[c.to] > maxActivity;
                 }
             }
             return need_rescaling;
@@ -121,7 +122,7 @@ template<typename T>
 //            increment /= decay;
 //        }
         
-        template<class Iterable>
+        template<class Iterable, concepts::scalar T>
         void update(Iterable &clause, const Solver<T>& solver) noexcept {
 
             bool normalize = false;
diff --git a/src/header/heuristics/impl/EventActivityMap.hpp b/src/header/heuristics/impl/EventActivityMap.hpp
index cfc009d136775e5f31a41271ad42da12f6015786..328854f9d093ee196055169c73895f83644b96b5 100755
--- a/src/header/heuristics/impl/EventActivityMap.hpp
+++ b/src/header/heuristics/impl/EventActivityMap.hpp
@@ -7,7 +7,7 @@
 
 #include <concepts>
 
-//#include "Scheduler.hpp"
+#include "util/traits.hpp"
 
 namespace tempo {
  
@@ -19,7 +19,6 @@ namespace tempo::heuristics::impl {
     /**
      * @brief Class that can be used to record the activity on distance constraints
      */
-template<typename T>
     class EventActivityMap {
     public:
         /**
@@ -27,7 +26,8 @@ template<typename T>
          * @tparam T type of scheduler
          * @param scheduler scheduler for which to construct the ActivityMap
          */
-        
+
+        template<concepts::scalar T>
         explicit EventActivityMap(Solver<T> &solver)
 //        : sched(scheduler)
         {
@@ -45,10 +45,12 @@ template<typename T>
          * @param edge constraint
          * @return
          */
+        template<concepts::scalar T>
         constexpr double get(const DistanceConstraint<T>& c) const noexcept {
             return numeric_activity[c.from] + numeric_activity[c.to];
         }
-        
+
+        template<concepts::scalar T>
         constexpr double get(const Literal<T> l,
                              const Solver<T> &solver) const noexcept {
           double a{0};
@@ -65,6 +67,7 @@ template<typename T>
           return a;
         }
 
+        template<concepts::scalar T>
         constexpr double get(const var_t x, const Solver<T>& solver) const noexcept {
           double a{boolean_activity[x]};
           //            double a{0};
@@ -127,7 +130,6 @@ template<typename T>
 
     protected:
 
-//        Scheduler<T>& sched;
         std::vector<double> numeric_activity{};
         std::vector<double> boolean_activity{};
     };
diff --git a/src/header/nn/GNNEdgePolarityPredictor.hpp b/src/header/nn/GNNEdgePolarityPredictor.hpp
index 7a873fe7fdd65e0e8197374982e1c9e498450ea0..e004edb722c8c11397f7d4823055be550df5b0e1 100644
--- a/src/header/nn/GNNEdgePolarityPredictor.hpp
+++ b/src/header/nn/GNNEdgePolarityPredictor.hpp
@@ -11,13 +11,25 @@
 #include "GraphBuilder.hpp"
 #include "util/Matrix.hpp"
 #include "util/traits.hpp"
-#include "util/parsing/format.hpp"
+#include "Literal.hpp"
+
+namespace tempo {
+    template<typename T>
+    class Solver;
+}
 
 namespace tempo::nn::heuristics {
+
+    namespace detail {
+        bool choosePolarityFromHeatMap(unsigned taskFrom, unsigned taskTo, const Matrix<DataType> &heatMap);
+    }
+
+
     /**
      * GNN based value selection heuristic that predicts a probability for each edge and returns the polarity of
      * choicepoint accordingly
      */
+    template<concepts::scalar T, SchedulingResource R>
     class GNNEdgePolarityPredictor {
     public:
         /**
@@ -29,39 +41,48 @@ namespace tempo::nn::heuristics {
          */
         GNNEdgePolarityPredictor(const std::filesystem::path &modelLocation,
                                  const std::filesystem::path &featureExtractorConfigLocation,
-                                 const ProblemInstance &problemInstance);
+                                 SchedulingProblemHelper<T, R> problemInstance) :
+                gnn(modelLocation),
+                graphBuilder(featureExtractorConfigLocation, std::move(problemInstance)) {}
 
         /**
          * Returns the choice point in the appropriate polarity according to a GNN edge regressor
-         * @param cp choicepoint to evaluate
-         * @return cp or ~cp according to the GNN heat map
+         * @param x variable for which to make a choice
+         * @return corresponding positive or negative literal according to the GNN
          */
-        template<typename Sched>
-        requires(traits::is_same_template_v<DistanceConstraint, decltype(std::declval<Sched>().getEdge(
-                std::declval<lit>()))>)
-        lit choose(tempo::var cp, const Sched &scheduler) const{
-            // @TODO does not work anymore!!!
-            auto [from, to] = scheduler.getEdge(POS(cp));
-            return choosePolarityFromHeatMap(from, to, edgeHeatMap) ? POS(cp) : NEG(cp);
+        auto choose(var_t x, const Solver<T> &solver) const -> Literal<T> {
+            auto edge = solver.boolean.getEdge(true, x);
+            auto edgeRev = solver.boolean.getEdge(false, x);
+            const auto &mapping = graphBuilder.getProblem().getMapping();
+            assert(mapping.contains(edge.from));
+            assert(mapping.contains(edge.to));
+            assert(mapping.contains(edgeRev.from));
+            assert(mapping.contains(edgeRev.to));
+            unsigned from = mapping(edge.from);
+            unsigned to = mapping(edge.to);
+            unsigned rfrom = mapping(edgeRev.from);
+            unsigned rto = mapping(edgeRev.to);
+            if (from != rto or to != rfrom) {
+                // @TODO not strictly necessary. simply allow distances between different tasks and use DST for probs
+                throw std::runtime_error("positive and negative edges have different tasks as endpoints.");
+            }
+
+            return solver.boolean.getLiteral(detail::choosePolarityFromHeatMap(from, to, edgeHeatMap), x);
         }
 
         /**
          * Runs the GNN to produce an edge heat map
-         * @param scheduler scheduler instance for which to produce the heat map
+         * @param solver scheduler instance for which to produce the heat map
          */
-        template<concepts::scalar T>
-        void preEvaluation(const Scheduler<T> &scheduler) {
-            auto graph = graphBuilder.getGraph(
-                    makeSolverState([&](event from, event to) { return scheduler.distance(from, to); }));
+        void preEvaluation(const Solver<T> &solver) {
+            auto taskNetwork = graphBuilder.getProblem().getTaskDistances(solver);
+            auto graph = graphBuilder.getGraph(makeSolverState(std::move(taskNetwork), solver));
             edgeHeatMap = gnn.getHeatMap(graph);
         }
 
-    protected:
-        static bool choosePolarityFromHeatMap(event from, event to, const Matrix<DataType> &heatMap);
-
     private:
         EdgeRegressor gnn;
-        GraphBuilder graphBuilder;
+        GraphBuilder<T, R> graphBuilder;
         Matrix<DataType> edgeHeatMap{};
     };
 }
diff --git a/src/header/nn/GraphBuilder.hpp b/src/header/nn/GraphBuilder.hpp
index 5b72bd263c94cc07cccfa7eadab3bca428d15676..04e756ab44b82730b7e23749b69816df59277a98 100644
--- a/src/header/nn/GraphBuilder.hpp
+++ b/src/header/nn/GraphBuilder.hpp
@@ -19,14 +19,18 @@
 #include "topology_extractors.hpp"
 #include "torch_types.hpp"
 #include "util/traits.hpp"
-#include "util/parsing/format.hpp"
+#include "util/SchedulingProblemHelper.hpp"
+#include "util/serialization.hpp"
 
 namespace tempo::nn {
 
-    MAKE_FACTORY_PATTERN(FeatureExtractor, nlohmann::json, TaskTimingFeatureExtractor, TimingEdgeExtractor,
+    MAKE_POLYMORPHIC_TYPE(FeatureExtractor, TaskTimingFeatureExtractor, TimingEdgeExtractor, ResourceEnergyExtractor)
+    MAKE_FACTORY_PATTERN(FeatureExtractor, const nlohmann::json&, TaskTimingFeatureExtractor, TimingEdgeExtractor,
                          ResourceEnergyExtractor)
 
-    MAKE_FACTORY_PATTERN(TopologyBuilder, ProblemInstance, MinimalTopologyBuilder)
+    MAKE_POLYMORPHIC_TYPE(TopologyBuilder, MinimalTopologyBuilder)
+    MAKE_T_FACTORY_PATTERN(TopologyBuilder, template<ESCAPE(typename T, typename R)>,
+                           ESCAPE(const SchedulingProblemHelper<T, R>&), MinimalTopologyBuilder)
 
     /**
      * Serializable type that holds the configuration of a feature extractor type
@@ -51,14 +55,8 @@ namespace tempo::nn {
      * Class that extracts all tensors necessary for running the inference of a GNN from a given problem and temporal
      * network
      */
+    template<concepts::scalar T, SchedulingResource R>
     class GraphBuilder {
-        template<typename EvtFun>
-        struct FactoryChecker {
-            HOLDS_FOR_ALL(FeatureExtractor, feature_extractor, EvtFun)
-            HOLDS_FOR_ALL(TopologyExtractor, topology_extractor, EvtFun)
-            static constexpr bool value = __FeatureExtractor_tester__<FeatureExtractor>::value and
-                                          __TopologyExtractor_tester__<TopologyBuilder>::value;
-        };
     public:
 
         /**
@@ -66,29 +64,40 @@ namespace tempo::nn {
          * @param configPath path to the feature extractor configurations
          * @param problemInstance Initial problem containing immutable information about tasks and resources
          */
-        GraphBuilder(const std::filesystem::path &configPath, const ProblemInstance &problemInstance);
+        GraphBuilder(const std::filesystem::path &configPath, SchedulingProblemHelper<T, R> problemInstance)
+                : problemDefinition(std::move(problemInstance)) {
+            auto data = serialization::deserializeFromFile<GraphBuilderConfig>(configPath);
+            topologyExtractor = TopologyBuilderFactory::getInstance().create(data.topologyExtractor.extractorName,
+                                                                             problemDefinition);
+            taskFeatureExtractor = FeatureExtractorFactory::getInstance().create(
+                    data.taskFeatureExtractor.extractorName, data.taskFeatureExtractor.arguments);
+            resourceFeatureExtractor = FeatureExtractorFactory::getInstance().create(
+                    data.resourceFeatureExtractor.extractorName, data.resourceFeatureExtractor.arguments);
+            edgeFeatureExtractor = FeatureExtractorFactory::getInstance().create(
+                    data.edgeFeatureExtractor.extractorName, data.edgeFeatureExtractor.arguments);
+        }
 
         /**
          * Extracts all features and topology information from a given solver state
-         * @tparam EvtFun callable that represents distances between events
+         * @tparam Args Types of solver state fields
+         * @param state current state of the solver
          * @return InputGraph containing topology and feature information
          */
-        template<concepts::arbitrary_event_dist_fun EvtFun>
-        auto getGraph(const SolverState<EvtFun>& state) -> InputGraph {
-            static_assert(FactoryChecker<EvtFun>::value,
-                          "Not all feature extractors or topology extractors have the correct signature");
+        template<typename ...Args>
+        auto getGraph(const SolverState<Args...> &state) -> InputGraph {
             auto topology = std::visit(
                     [&](auto &extractor) { return extractor.getTopology(state); },
                     *topologyExtractor);
-            auto taskFeats = std::visit(
-                    [t = std::cref(topology), &state](const auto &extractor) { return extractor(t, state.eventNetwork); },
-                    taskFeatureExtractor);
-            auto edgeFeats = std::visit(
-                    [t = std::cref(topology), &state](const auto &extractor) { return extractor(t, state.eventNetwork); },
-                    edgeFeatureExtractor);
-            auto resourceFeats = std::visit(
-                    [t = std::cref(topology), &state](const auto &extractor) { return extractor(t, state.eventNetwork); },
-                    resourceFeatureExtractor);
+            const auto &p = problemDefinition;
+            auto taskFeats = std::visit([t = std::cref(topology), &p, &state](const auto &extractor) {
+                return extractor(t, state, p);
+            }, taskFeatureExtractor);
+            auto edgeFeats = std::visit([t = std::cref(topology), &p, &state](const auto &extractor) {
+                return extractor(t, state, p);
+            }, edgeFeatureExtractor);
+            auto resourceFeats = std::visit([t = std::cref(topology), &p, &state](const auto &extractor) {
+                return extractor(t, state, p);
+            }, resourceFeatureExtractor);
             InputGraph ret;
             using K = GraphKeys;
             ret.insert(K::TaskFeatures, std::move(taskFeats));
@@ -102,8 +111,17 @@ namespace tempo::nn {
             return ret;
         }
 
+        /**
+         * gets the stored problem definition
+         * @return problem definition
+         */
+        auto getProblem() const noexcept -> const SchedulingProblemHelper<T, R>& {
+            return problemDefinition;
+        }
+
 
     private:
+        SchedulingProblemHelper<T, R> problemDefinition;
         std::optional<TopologyBuilder> topologyExtractor;
         FeatureExtractor taskFeatureExtractor;
         FeatureExtractor edgeFeatureExtractor;
diff --git a/src/header/nn/feature_extractors.hpp b/src/header/nn/feature_extractors.hpp
index 6ed8c373bdb3f1f52a947a8df7fa580ced027f70..a61c936c8925e829ad730daaa489dff869f218be 100644
--- a/src/header/nn/feature_extractors.hpp
+++ b/src/header/nn/feature_extractors.hpp
@@ -15,7 +15,7 @@
 
 #include "util/traits.hpp"
 #include "util/serialization.hpp"
-#include "util/task_timings.hpp"
+#include "util/SchedulingProblemHelper.hpp"
 #include "tensor_utils.hpp"
 #include "torch_types.hpp"
 #include "util/factory_pattern.hpp"
@@ -24,17 +24,6 @@
  * @brief namespace containing neural network specific code
  */
 namespace tempo::nn {
-    /**
-     * @brief Requirement for a type that can be used as resource or task feature extractor
-     * @details @copybrief
-     * Requires a call operator that takes a graph topology and an event network and returns a tensor containing all
-     * features
-     * @tparam Extractor
-     */
-    template<typename Extractor, typename EvtFun>
-    concept feature_extractor = concepts::callable_r<Extractor, torch::Tensor, const Topology, const EvtFun>
-            and concepts::arbitrary_event_dist_fun<EvtFun>;
-
     /**
      * @brief Extracts features from timing information of tasks.
      * @details @copybrief
@@ -42,23 +31,32 @@ namespace tempo::nn {
      * the origin and horizon.
      */
     struct TaskTimingFeatureExtractor {
+        static constexpr auto LegacyKey = "legacy";
+        bool legacyFeatures = false;
+
         /**
          * Returns a tensor containing timing features for all tasks.
+         * @tparam DistFun type of distance function
+         * @tparam S Solver type that has a member that provides upper and lower bounds
+         * @tparam T timing type
+         * @tparam R scheduling resource type
          * @param topology Graph topology for which to extract the features
-         * @param eventDistances current event network from which the features are generated
+         * @param state current state of the solver
+         * @param problem scheduling problem description
          * @return torch::Tensor containing task features
          */
-        template<concepts::arbitrary_event_dist_fun EvtFun>
-        auto operator()(const Topology &topology, const EvtFun &eventDistances) const -> torch::Tensor {
-            using namespace torch::indexing;
-            const auto ub = upperBound(eventDistances);
+        template<typename DistFun, concepts::distance_provider S, concepts::scalar T, SchedulingResource R>
+        auto operator()(const Topology &topology, const SolverState<DistFun, S> &state,
+                        const SchedulingProblemHelper<T, R> &problem) const -> torch::Tensor {
+            const auto ub = static_cast<DataType>(problem.schedule().getLatestEnd(state.solver));
             torch::Tensor ret = torch::empty({static_cast<long>(topology.numTasks), 4}, dataTensorOptions());
-            for (long t = 0; t < topology.numTasks; ++t) {
-                util::sliceAssign(ret, {static_cast<long>(t), util::SliceHere},
-                                  {minDuration(t, eventDistances)/ static_cast<DataType>(ub),
-                                   maxDuration(t, eventDistances) / static_cast<DataType>(ub),
-                                   earliestStartTime(t, eventDistances) / static_cast<DataType>(ub),
-                                   latestCompletion(t, eventDistances) / static_cast<DataType>(ub)});
+            for (auto [t, task]: iterators::enumerate(problem.tasks(), 0l)) {
+                util::sliceAssign(ret, {t, util::SliceHere},
+                                  {task.minDuration(state.solver) / ub,
+                                   task.maxDuration(state.solver) / ub,
+                                   task.getEarliestStart(state.solver) / ub *
+                                   static_cast<DataType>(1 - 2 * legacyFeatures),
+                                   task.getLatestEnd(state.solver) / ub - static_cast<DataType>(legacyFeatures)});
             }
 
             return ret;
@@ -76,22 +74,27 @@ namespace tempo::nn {
     struct ResourceEnergyExtractor {
         /**
          * Returns a tensor containing resource energy feature for all resources.
+         * @tparam DistFun type of distance function
+         * @tparam S Solver type that has a member that provides upper and lower bounds
+         * @tparam T timing type
+         * @tparam R scheduling resource type
          * @param topology Graph topology for which to extract the features
-         * @param eventDistances current event network from which the features are generated
+         * @param state current state of the solver
+         * @param problem scheduling problem description
          * @return torch::Tensor containing resource features
          */
-        template<concepts::arbitrary_event_dist_fun EvtFun>
-        auto operator()(const Topology &topology, const EvtFun &eventDistances) const -> torch::Tensor {
-            using namespace torch::indexing;
-            const auto ub = upperBound(eventDistances);
+        template<typename Dist, concepts::distance_provider S, concepts::scalar T, SchedulingResource R>
+        auto operator()(const Topology &topology, const SolverState<Dist, S> &state,
+                        const SchedulingProblemHelper<T, R> &problem) const -> torch::Tensor {
+            const auto ub = static_cast<DataType>(problem.schedule().getLatestEnd(state.solver));
             auto ret = torch::zeros({static_cast<long>(topology.numResources), 1}, dataTensorOptions());
             const std::span demands(topology.resourceDemands.data_ptr<DataType>(), topology.resourceDemands.size(0));
             const auto tasks = util::getIndexSlice(topology.resourceDependencies, 0);
             const auto resources = util::getIndexSlice(topology.resourceDependencies, 1);
             std::span energy(ret.data_ptr<DataType>(), topology.numResources);
             for (auto [task, res, demand] : iterators::zip(tasks, resources, demands)) {
-                auto avgDuration = static_cast<DataType>(minDuration(task, eventDistances)
-                        + maxDuration(task, eventDistances)) / DataType(2) / ub;
+                auto avgDuration = static_cast<DataType>(problem.tasks()[task].minDuration(state.solver)
+                        + problem.tasks()[task].maxDuration(state.solver)) / DataType(2) / ub;
                 energy[res] += avgDuration * demand;
             }
 
@@ -108,30 +111,39 @@ namespace tempo::nn {
     struct TimingEdgeExtractor {
         /**
          * Returns a tensor containing timing features for all edges.
+         * @tparam DistFun type of distance function
+         * @tparam S Solver type that has a member that provides upper and lower bounds
+         * @tparam T timing type
+         * @tparam R scheduling resource type
          * @param topology Graph topology for which to extract the features
-         * @param eventDistances current event network from which the features are generated
+         * @param state current state of the solver
+         * @param problem scheduling problem description
          * @return torch::Tensor containing edge features
          */
-        template<concepts::arbitrary_event_dist_fun EvtFun>
-        auto operator()(const Topology &topology, const EvtFun &eventDistances) const -> torch::Tensor {
-            using namespace torch::indexing;
-            const auto ub = upperBound(eventDistances);
+        template<concepts::arbitrary_task_dist_fun Dist, typename S, concepts::scalar T, SchedulingResource R>
+        auto operator()(const Topology &topology, const SolverState<Dist, S> &state,
+                        const SchedulingProblemHelper<T, R> &problem) const -> torch::Tensor {
+            const auto ub = static_cast<DataType>(problem.schedule().getLatestEnd(state.solver));
             const auto edgeFrom = util::getIndexSlice(topology.edgeIndices, 0);
             const auto edgeTo = util::getIndexSlice(topology.edgeIndices, 1);
             auto ret = torch::empty({static_cast<long>(edgeFrom.size()), 2}, dataTensorOptions());
             for (auto [idx, from, to] : iterators::zip_enumerate(edgeFrom, edgeTo, 0l)) {
-                const auto tempDiff = taskDistance(from, to, eventDistances);
-                const auto tempDiffRev = taskDistance(to, from, eventDistances);
+                const auto tempDiff = state.distance(from, to);
+                const auto tempDiffRev = state.distance(to, from);
                 const bool isPrecedence = tempDiff <= 0 or tempDiffRev <= 0;
                 util::sliceAssign(ret, {idx, util::SliceHere}, {static_cast<DataType>(isPrecedence),
-                                                                tempDiff / static_cast<DataType>(ub)});
+                                                                static_cast<DataType>(tempDiff) / ub});
             }
 
             return ret;
         }
     };
 
-    MAKE_DEFAULT_FACTORY(TaskTimingFeatureExtractor, const nlohmann::json&)
+    MAKE_FACTORY(TaskTimingFeatureExtractor, const nlohmann::json &config) {
+            return TaskTimingFeatureExtractor{.legacyFeatures = config.at(
+                    TaskTimingFeatureExtractor::LegacyKey).get<bool>()};
+        }
+    };
     MAKE_DEFAULT_FACTORY(ResourceEnergyExtractor, const nlohmann::json&)
     MAKE_DEFAULT_FACTORY(TimingEdgeExtractor, const nlohmann::json&)
 }
diff --git a/src/header/nn/topology_extractors.hpp b/src/header/nn/topology_extractors.hpp
index e8a1f3966d0db525fead1310804db7d2ed240e30..e8bffa2db06bca8a04c0769d3dabd7991540c09e 100644
--- a/src/header/nn/topology_extractors.hpp
+++ b/src/header/nn/topology_extractors.hpp
@@ -6,54 +6,21 @@
 #ifndef TEMPO_TOPOLOGY_EXTRACTORS_HPP
 #define TEMPO_TOPOLOGY_EXTRACTORS_HPP
 
-#include <ranges>
 #include <vector>
+#include <cassert>
 
 #include "Global.hpp"
-#include "util/parsing/format.hpp"
+#include "Model.hpp"
+#include "util/SchedulingProblemHelper.hpp"
 #include "torch_types.hpp"
 #include "tensor_utils.hpp"
 #include "util/Matrix.hpp"
 #include "util/factory_pattern.hpp"
+#include "util/traits.hpp"
 #include "DistanceConstraint.hpp"
 
 namespace tempo::nn {
 
-    /**
-     * Represents the state of a the solver that can be used to extract the graph topology
-     * @tparam EvtFun type of event distance function
-     */
-    template<concepts::arbitrary_event_dist_fun EvtFun>
-    struct SolverState {
-        template<typename E>
-        requires(concepts::arbitrary_event_dist_fun<std::remove_cvref_t<E>>)
-        explicit constexpr SolverState(E &&evt) : eventNetwork(std::forward<E>(evt)) {}
-        EvtFun eventNetwork; ///< event network functor object
-    };
-
-    /**
-     * Concept that models the interface of a topology extractor
-     * @tparam Extractor
-     * @tparam EvtFun type of event function
-     */
-    template<typename Extractor, typename EvtFun>
-    concept topology_extractor = requires(Extractor e, const SolverState<EvtFun> &state) {
-        { e.getTopology(state) } -> std::convertible_to<Topology>;
-    };
-
-
-    /**
-     * Helper function for SolverState
-     * @tparam Args argument types to SolverState
-     * @param args arguments to SolverState
-     * @return solver state view of the arguments. Depending on the reference type of the arguments, this is only
-     * a non owning object
-     */
-    template<typename ...Args>
-    constexpr auto makeSolverState(Args &&...args) {
-        return SolverState<Args...>(std::forward<Args>(args)...);
-    }
-
     namespace impl {
         struct EdgeLookup : protected Matrix<IndexType> {
             static constexpr IndexType NoValue = std::numeric_limits<IndexType>::lowest();
@@ -109,17 +76,36 @@ namespace tempo::nn {
          * Ctor
          * @param problem initial problem instance
          */
-        explicit MinimalTopologyBuilder(const ProblemInstance &problem);
+        template<concepts::scalar T, SchedulingResource R>
+        explicit MinimalTopologyBuilder(const SchedulingProblemHelper<T, R> &problem) {
+            using namespace iterators;
+            cache.numTasks = problem.tasks().size();
+            cache.numResources = problem.resources().size();
+            impl::TopologyData topologyData{.edgeLookup = impl::EdgeLookup(cache.numTasks * 2 + 2)};
+            for (auto [r, resourceSpec]: enumerate(problem.resources(), 0l)) {
+                completeSubGraph(resourceSpec, r, problem.getMapping(), topologyData);
+            }
+
+            addPrecedenceEdges(problem.precedences(), problem.getMapping(), topologyData);
+            cache.edgeIndices = util::makeIndexTensor(topologyData.edges);
+            cache.edgePairMask = torch::from_blob(topologyData.edgePairMask.data(),
+                                                  static_cast<long>(topologyData.edgePairMask.size()),
+                                                  indexTensorOptions()).clone();
+            cache.edgeResourceRelations = util::makeIndexTensor(topologyData.edgeIdx, topologyData.edgeRelResIdx);
+            cache.resourceDependencies = util::makeIndexTensor(topologyData.taskIdx, topologyData.resIdx);
+            cache.resourceDemands = torch::from_blob(topologyData.resDemands.data(),
+                                                     {static_cast<long>(topologyData.taskIdx.size()), 1},
+                                                     dataTensorOptions()).clone();
+        }
 
         /**
          * Generates a graph topology based on the initial problem instance and on the given decided precedence
          * constraints
-         * @tparam EvtFun type of event distance function
          * @param precedences range of decided precedences
          * @return the graph topology representing the initial problem and the added precedences
          */
-        template<typename EvtFun>
-        [[nodiscard]] auto getTopology(const SolverState<EvtFun> &) const -> const Topology &{
+        template<typename ...Args>
+        [[nodiscard]] auto getTopology(const SolverState<Args...> &) const -> const Topology &{
             return cache;
         }
 
@@ -130,12 +116,12 @@ namespace tempo::nn {
         [[nodiscard]] auto getTopology() const -> const Topology&;
 
     protected:
-        template<std::ranges::range R>
-        static void addPrecedenceEdges(const R &precedences, impl::TopologyData &topologyData) {
-            for (auto [from, to, dist] : precedences) {
-                assert(from != ORIGIN and from != HORIZON and to != ORIGIN and to != HORIZON &&
-                       "You probably passed a precedence between tasks instead of events!");
-                Edge e(TASK(from), TASK(to));
+        template<concepts::ttyped_range<DistanceConstraint> R>
+        static void addPrecedenceEdges(const R &precedences, const VarTaskMapping &vtMapping,
+                                       impl::TopologyData &topologyData) {
+            for (const auto &p : precedences) {
+                assert(vtMapping.contains(p.from) and vtMapping.contains(p.to));
+                Edge e(vtMapping(p.from), vtMapping(p.to));
                 if (e.first == e.second or topologyData.edgeLookup.contains(e)) {
                     continue;
                 }
@@ -146,16 +132,43 @@ namespace tempo::nn {
 
         static void addEdge(const Edge &e, bool isResourceEdge, IndexType maskVal, impl::TopologyData &topologyData);
 
-        static void completeSubGraph(const Resource<int> &resourceSpec, IndexType resource,
-                                     impl::TopologyData &topologyData);
+        template<SchedulingResource R>
+        static void completeSubGraph(const R &resourceSpec, IndexType resource, const VarTaskMapping &vtMapping,
+                                     impl::TopologyData &topologyData) {
+            auto taskId = [&vtMapping](const auto &t) { return vtMapping(t.start.id()); };
+            const auto &tasks = resourceSpec;
+            for (std::size_t i = 0; i < tasks.size(); ++i) {
+                topologyData.taskIdx.emplace_back(taskId(tasks[i]));
+                topologyData.resIdx.emplace_back(resource);
+                topologyData.resDemands.emplace_back(static_cast<DataType>(resourceSpec.getDemand(i)) /
+                                                     static_cast<DataType>(resourceSpec.resourceCapacity()));
+                for (std::size_t j = i + 1; j < tasks.size(); ++j) {
+                    Edge e(taskId(tasks[i]), taskId(tasks[j]));
+                    Edge rev(taskId(tasks[j]), taskId(tasks[i]));
+                    topologyData.edgeRelResIdx.emplace_back(resource);
+                    topologyData.edgeRelResIdx.emplace_back(resource);
+                    if (topologyData.edgeLookup.contains(e)) {
+                        topologyData.edgeIdx.emplace_back(topologyData.edgeLookup(e));
+                        topologyData.edgeIdx.emplace_back(topologyData.edgeLookup(rev));
+                        continue;
+                    }
+
+                    addEdge(e, true, topologyData.pairMaskVal, topologyData);
+                    addEdge(rev, true, topologyData.pairMaskVal, topologyData);
+                    ++topologyData.pairMaskVal;
+                }
+            }
+        }
 
     private:
         Topology cache;
     };
 
-    MAKE_FACTORY(MinimalTopologyBuilder, const ProblemInstance &problemInstance) {
-        return MinimalTopologyBuilder(problemInstance);
-    }};
+    MAKE_TEMPLATE_FACTORY(MinimalTopologyBuilder, ESCAPE(concepts::scalar T, SchedulingResource R),
+                          const ESCAPE(SchedulingProblemHelper<T, R>) &problemInstance) {
+            return MinimalTopologyBuilder(problemInstance);
+        }
+    };
 }
 
 #endif //TEMPO_TOPOLOGY_EXTRACTORS_HPP
diff --git a/src/header/nn/torch_types.hpp b/src/header/nn/torch_types.hpp
index 0b02bcee66b80b173d8bee20530a62b9e0a5b6d0..9505728bb58ac01a2b3b22a21c42f69d0879c106 100644
--- a/src/header/nn/torch_types.hpp
+++ b/src/header/nn/torch_types.hpp
@@ -10,6 +10,8 @@
 #include <torch/types.h>
 #include <vector>
 
+#include "util/traits.hpp"
+
 
 namespace tempo::nn {
 #ifdef TORCH_USE_GPU
@@ -70,6 +72,41 @@ namespace tempo::nn {
                                             ResourceDependencies, EdgeResourceRelations, EdgePairMask};
     };
 
+    /**
+     * Represents the state of a the solver that can be used to extract the graph topology
+     */
+    template<concepts::arbitrary_task_dist_fun Dist, typename S>
+    struct SolverState {
+        template<typename D, typename Sol>
+        explicit constexpr
+        SolverState(D &&distance, Sol &&solver): distance(std::forward<D>(distance)), solver(std::forward<Sol>(solver)) {}
+        Dist distance;
+        S solver;
+    };
+
+    /**
+     * Concept that models the interface of a topology extractor
+     * @tparam Extractor
+     * @tparam Args template parameters for solver state
+     */
+    template<typename Extractor, typename ...Args>
+    concept topology_extractor = requires(Extractor e, const SolverState<Args...> &state) {
+        { e.getTopology(state) } -> std::convertible_to<Topology>;
+    };
+
+
+    /**
+     * Helper function for SolverState
+     * @tparam Args argument types to SolverState
+     * @param args arguments to SolverState
+     * @return solver state view of the arguments. Depending on the reference type of the arguments, this is only
+     * a non owning object
+     */
+    template<typename ...Args>
+    constexpr auto makeSolverState(Args &&...args) noexcept {
+        return SolverState<Args...>(std::forward<Args>(args)...);
+    }
+
 
 }
 
diff --git a/src/header/util/Options.hpp b/src/header/util/Options.hpp
index 1ef0ed73e0f870f8cdc50ee27257ac3c86c5472c..b15702b1e50bab603ad019736e9432451bb1f0f1 100755
--- a/src/header/util/Options.hpp
+++ b/src/header/util/Options.hpp
@@ -23,16 +23,71 @@
 #define __CMDLINE_HPP
 
 #include <tclap/CmdLine.h>
+#include <limits>
+#include <memory>
+
+
+struct argbase {
+    virtual ~argbase() {}
+    virtual void assign() = 0;
+};
+
+template <typename Opt, typename ClapArg, typename E = void>
+struct arg : public argbase {
+    ClapArg carg;
+    Opt &opt;
+
+    template <typename... T>
+    arg(TCLAP::CmdLine &cmd, Opt &opt, T &&...args)
+            : carg(std::forward<T>(args)...), opt(opt) {
+        cmd.add(carg);
+    }
+
+    void assign() override { opt = carg.getValue(); }
+};
+
+template<typename Opt, typename ClapArg>
+struct arg<Opt, ClapArg, typename std::enable_if<std::is_enum<Opt>{}>::type> : public argbase {
+    ClapArg carg;
+    Opt &opt;
+
+    template<typename... T>
+    arg(TCLAP::CmdLine &cmd, Opt &opt, T &&...args): carg(std::forward<T>(args)...), opt(opt) {
+        cmd.add(carg);
+    }
+
+    void assign() override {
+        opt = static_cast<typename std::remove_reference<Opt>::type>(carg.getValue());
+    }
+};
+
+
+struct cmdline {
+    TCLAP::CmdLine cmd;
+    std::vector<std::unique_ptr<argbase>> args;
+
+    explicit cmdline(const std::string &message, char delimiter = ' ',
+                     const std::string &version = "none", bool helpAndVersion = true);
+
+    template <typename ClapArg, typename Opt, typename... T>
+    void add(Opt &opt, T &&...clapargs) {
+        args.emplace_back(std::move(std::make_unique<arg<Opt, ClapArg>>(
+        cmd, opt, std::forward<T>(clapargs)...)));
+    }
+
+    void parse(int argc, char *argv[]);
+};
+
 
 namespace tempo {
 
 class Options {
 
 public:
-    Options() {}
+    Options() = default;
     
   // the actual options
-  std::string cmdline{""}; // for reference
+  std::string cmdline{}; // for reference
   std::string instance_file{"../data/osp/hurley/j7per0-0.txt"};
 
   enum verbosity { SILENT = 0, QUIET, NORMAL, YACKING, SOLVERINFO };
@@ -96,6 +151,21 @@ public:
 
 Options parse(int argc, char *argv[]);
 
+class Parser {
+    std::unique_ptr<Options> options;
+    std::unique_ptr<cmdline> cmdLine;
+public:
+    explicit Parser(const std::string &name = "tempo");
+    auto getOptions() noexcept -> Options &;
+    [[nodiscard]] auto getOptions() const noexcept -> const Options &;
+    auto getCmdLine() noexcept -> cmdline &;
+    [[nodiscard]] auto getCmdLine() const noexcept -> const cmdline &;
+    void parse(int argc, char **argv);
+};
+
+auto getBaseParser() -> Parser;
+
+
 static Options no_option;
 } // namespace tempo
 
diff --git a/src/header/util/SchedulingProblemHelper.hpp b/src/header/util/SchedulingProblemHelper.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d46162f1ee83c4cc035e43bc85d45b6abf0d83e5
--- /dev/null
+++ b/src/header/util/SchedulingProblemHelper.hpp
@@ -0,0 +1,268 @@
+/**
+* @author Tim Luchterhand
+* @date 01.07.24
+* @brief utility for extracting timing information between tasks
+*/
+
+#ifndef TEMPO_SCHEDULINGPROBLEMHELPER_HPP
+#define TEMPO_SCHEDULINGPROBLEMHELPER_HPP
+
+#include <concepts>
+#include <limits>
+#include <Iterators.hpp>
+#include <vector>
+#include <ranges>
+#include <algorithm>
+#include <cassert>
+#include <stdexcept>
+
+#include "DirectedGraph.hpp"
+#include "Model.hpp"
+#include "util/Matrix.hpp"
+#include "util/traits.hpp"
+
+namespace tempo {
+
+    namespace detail {
+        template<concepts::scalar T>
+        struct NoValue {
+            static constexpr auto value() {
+                if constexpr (std::floating_point<T>) {
+                    return std::numeric_limits<T>::infinity();
+                } else {
+                    return std::numeric_limits<T>::max();
+                }
+            }
+        };
+
+        template<concepts::scalar T>
+        inline constexpr auto NoDistance = detail::NoValue<T>::value();
+
+        template<typename T, typename Arc>
+        void getDistances(Matrix<T> &matrix, const std::vector<std::vector<Arc>> &adjacency) noexcept {
+            for (auto [src, neighbors] : iterators::enumerate(adjacency)) {
+                for (const auto &dest : neighbors) {
+                    matrix(src, dest) = dest.label();
+                }
+            }
+        }
+
+        template<typename Arc>
+        concept labeled_arc = concepts::same_template<Arc, LabeledEdge> or
+                              concepts::same_template<Arc, StampedLabeledEdge>;
+
+        template<typename P, typename T>
+        concept bound_provider = requires(const P instance, var_t var) {
+            { instance.upper(var) } -> std::same_as<T>;
+            { instance.lower(var) } -> std::same_as<T>;
+        };
+
+        template<concepts::scalar T, bound_provider<T> BoundProvider>
+        class TaskDistanceFunction {
+            const std::vector<Interval<T>> &tasks;
+            const BoundProvider &boundProvider;
+            Matrix<T> graphDistances;
+        public:
+
+            template<labeled_arc Arc>
+            TaskDistanceFunction(const std::vector<Interval<T>> &tasks, const DirectedGraph<Arc> &varGraph,
+                                 const BoundProvider &boundProvider) :
+                    tasks(tasks), boundProvider(boundProvider),
+                    graphDistances(varGraph.size(), varGraph.size(), NoDistance<T>) {
+                getDistances(graphDistances, varGraph.forward());
+                getDistances(graphDistances, varGraph.backward());
+            }
+
+            [[nodiscard]] T operator()(unsigned taskFrom, unsigned taskTo) const {
+                const auto srcVar = tasks[taskFrom].start;
+                const auto destVar = tasks[taskTo].end;
+                const auto boundDistance = boundProvider.upper(srcVar.id()) - boundProvider.lower(destVar.id());
+                return std::min(graphDistances(srcVar.id(), destVar.id()), boundDistance) + destVar.offset();
+            }
+        };
+    }
+
+    template<typename T>
+    concept SchedulingResource = concepts::ttyped_range<T, Interval> and requires(const T instance, unsigned taskId) {
+        { instance.resourceCapacity() } -> concepts::scalar;
+        { instance.getDemand(taskId) } -> concepts::scalar;
+    };
+
+    /**
+     * Mapping from variable id to task id
+     */
+    class VarTaskMapping {
+        std::vector<unsigned> varToTask;
+        var_t offset{};
+        static constexpr unsigned NoTask = std::numeric_limits<unsigned>::max();
+    public:
+        static constexpr unsigned NumTemporalVarPerTask = 3;
+        VarTaskMapping() = default;
+
+        /**
+         * Ctor
+         * @tparam Tasks range containing tempo::Interval
+         * @param tasks all tasks of the problem
+         */
+        template<concepts::ttyped_range<tempo::Interval> Tasks>
+        requires(std::ranges::sized_range<Tasks>)
+        constexpr explicit VarTaskMapping(const Tasks &tasks) :
+                varToTask(NumTemporalVarPerTask * std::ranges::size(tasks), NoTask),
+                offset(std::numeric_limits<var_t>::max()) {
+
+            var_t max = 0;
+            for (const auto &task : tasks) {
+                max = std::max({max, task.start.id(), task.end.id()});
+                offset = std::min({offset, task.start.id(), task.end.id()});
+            }
+
+            if (not varToTask.empty() and max - offset >= NumTemporalVarPerTask * std::ranges::size(tasks)) {
+                throw std::runtime_error("expected continuous ranges of variable ids");
+            }
+
+            for (auto [idx, task]: iterators::enumerate(tasks)) {
+                varToTask[task.start.id() - offset] = idx;
+                varToTask[task.end.id() - offset] = idx;
+            }
+        }
+
+        /**
+         * Mapping from variable id to task
+         * @param variable id of the variable
+         * @return corresponding task id
+         */
+        [[nodiscard]] unsigned operator()(var_t variable) const noexcept;
+
+        /**
+         * Whether the mapping contains a variable
+         * @param variable id of the variable
+         * @return true if variable is in mapping, false otherwise
+         */
+        [[nodiscard]] bool contains(var_t variable) const noexcept;
+
+    };
+
+    template<typename T>
+    class Solver;
+
+    /**
+     * Class that represents a scheduling problem and provides helper functions
+     */
+    template<concepts::scalar T, SchedulingResource R>
+    class SchedulingProblemHelper {
+        using TaskVec = std::vector<Interval<T>>;
+        using RVec = std::vector<R>;
+        using PrecVec = std::vector<DistanceConstraint<T>>;
+        TaskVec t;
+        RVec res;
+        PrecVec precs;
+        VarTaskMapping v2t;
+        Interval<T> sched;
+
+    public:
+        SchedulingProblemHelper() = default;
+
+        /**
+         * CTor
+         * @param tasks tasks contained in the problem
+         * @param resources resources in the problem
+         * @param precedences precedences between variables in the problem
+         * @param schedule Interval that represents the entire schedule
+         */
+        SchedulingProblemHelper(TaskVec tasks, RVec resources, PrecVec precedences, Interval<T> schedule) :
+                t(std::move(tasks)), res(std::move(resources)), precs(std::move(precedences)), v2t(t), sched(schedule) {}
+
+        /**
+         * read only access to the tasks
+         * @return vector with the tasks
+         */
+        [[nodiscard]] auto tasks() const noexcept -> const TaskVec & {
+            return t;
+        }
+
+        /**
+         * read only access to the resources
+         * @return vector with the resources
+         */
+        [[nodiscard]] auto resources() const noexcept -> const RVec & {
+            return res;
+        }
+
+        /**
+         * read only access to the precedences
+         * @return vector with the precedences
+         */
+        [[nodiscard]] auto precedences() const noexcept -> const PrecVec & {
+            return precs;
+        }
+
+        /**
+         * @return task associated with the variable
+         * @param variable id of a variable
+         * @note UB if variable not associated with a task
+         */
+        [[nodiscard]] auto getTask(var_t variable) const noexcept -> TaskVec::const_reference {
+            return t[v2t(variable)];
+        }
+
+        /**
+         * Whether a given task id belongs to the problem
+         * @param taskId id of a task
+         * @return true if task id exists in problem, false otherwise
+         */
+        [[nodiscard]] bool hasTask(unsigned taskId) const noexcept {
+            return taskId < t.size();
+        }
+
+        /**
+         * Whether a given variable id is associated with a task
+         * @param variable id of a variable
+         * @return whether the variable is contained in the variable-to-task mapping
+         */
+        [[nodiscard]] bool hasVariable(var_t variable) const noexcept {
+            return v2t.contains(variable);
+        }
+
+        /**
+         * gets the mapping function from tasks to variables
+         * @return mapping from variables to tasks
+         */
+        [[nodiscard]] auto getMapping() const noexcept -> VarTaskMapping {
+            return v2t;
+        }
+
+        /**
+         * Access to the interval representing the entire schedule
+         */
+        auto schedule() const noexcept -> const Interval<T> & {
+            return sched;
+        }
+
+        /**
+         * Gets a functor object that represents temporal distances between tasks. The distance between two tasks t1 and
+         * t2 is equal to dist(start_t1, end_t2)
+         * @tparam BoundProvider Class that provides upper and lower bounds for numeric variables
+         * @tparam Arc Arc type that contains a distance label
+         * @param varGraph directed graph representing distances between variables
+         * @param boundProvider object that provides upper and lower bounds
+         * @return distance function object that provides distances between tasks
+         */
+        template<detail::bound_provider<T> BoundProvider, detail::labeled_arc Arc>
+        [[nodiscard]] auto getTaskDistances(const DirectedGraph<Arc> &varGraph,
+                                            const BoundProvider &boundProvider) const {
+            return detail::TaskDistanceFunction<T, BoundProvider>(t, varGraph, boundProvider);
+        }
+
+        /**
+         * Function overload of getTaskDistances
+         * @param solver solver instance used to construct the task distance function
+         * @return distance function object that provides distances between tasks
+         */
+        [[nodiscard]] auto getTaskDistances(const Solver<T> &solver) const {
+            using BP = std::remove_cvref_t<decltype(solver.numeric)>;
+            return detail::TaskDistanceFunction<T, BP>(t, solver.core, solver.numeric);
+        }
+    };
+}
+
+#endif //TEMPO_SCHEDULINGPROBLEMHELPER_HPP
diff --git a/src/header/util/factory_pattern.hpp b/src/header/util/factory_pattern.hpp
index ef1cf44b1eaa6deb71a057bdc6c6ce82e9d54753..7ff8a137f3766b8022fef28b3a657faa02fbb895 100644
--- a/src/header/util/factory_pattern.hpp
+++ b/src/header/util/factory_pattern.hpp
@@ -9,6 +9,8 @@
 #include <string>
 #include <variant>
 
+#define ESCAPE(...) __VA_ARGS__
+
 #define GET_MACRO(_1, _2, _3, _4, _5, _6, NAME, ...) NAME
 #define TYPE_ENTRY(...) GET_MACRO(__VA_ARGS__, \
 TYPE_ENTRY6, TYPE_ENTRY5, TYPE_ENTRY4, TYPE_ENTRY3, TYPE_ENTRY2, TYPE_ENTRY1, TYPE_ENTRY0)(__VA_ARGS__)
@@ -39,7 +41,6 @@ FACTORY_TYPE6, FACTORY_TYPE5, FACTORY_TYPE4, FACTORY_TYPE3, FACTORY_TYPE2, FACTO
   };
 
 #define MAKE_T_FACTORY_PATTERN(TYPE_NAME, T_HEADER, CTOR_ARG, ...)             \
-  MAKE_POLYMORPHIC_TYPE(TYPE_NAME, __VA_ARGS__)                                \
   class TYPE_NAME##Factory final {                                             \
     using TYPE_NAME##FactoryType = std::variant<FACTORY_ENTRY(__VA_ARGS__)>;   \
                                                                                \
@@ -53,7 +54,7 @@ FACTORY_TYPE6, FACTORY_TYPE5, FACTORY_TYPE4, FACTORY_TYPE3, FACTORY_TYPE2, FACTO
       return instance;                                                         \
     }                                                                          \
     T_HEADER                                                                   \
-    auto create(const std::string &typeName, const CTOR_ARG &arguments) const  \
+    auto create(const std::string &typeName, CTOR_ARG arguments) const  \
         -> TYPE_NAME {                                                         \
       const auto &constructor = registry.at(typeName);                         \
       return std::visit(                                                       \
@@ -77,12 +78,16 @@ struct TYPE##Factory {                                           \
 #define MAKE_FACTORY_PATTERN(TYPE_NAME, CTOR_ARG, ...)                         \
   MAKE_T_FACTORY_PATTERN(TYPE_NAME, , CTOR_ARG, __VA_ARGS__)
 
-#define MAKE_FACTORY(TYPE, ...)         \
-struct TYPE##Factory {                  \
-    static TYPE create(__VA_ARGS__)
+#define MAKE_P_FACTORY(TYPE, P_TYPE, ...)       \
+struct TYPE##Factory {                          \
+    static P_TYPE create(__VA_ARGS__)
+
+#define MAKE_TEMPLATE_P_FACTORY(TYPE, P_TYPE, T_ARG, ARG)                        \
+  struct TYPE##Factory {                                                         \
+    template <T_ARG> static P_TYPE create(ARG)
+
+#define MAKE_FACTORY(TYPE, ...) MAKE_P_FACTORY(TYPE, auto, __VA_ARGS__)
 
-#define MAKE_TEMPLATE_FACTORY(TYPE, T_ARG, ARG)                                \
-  struct TYPE##Factory {                                                       \
-    template <T_ARG> static TYPE create(ARG)
+#define MAKE_TEMPLATE_FACTORY(TYPE, T_ARG, ARG) MAKE_TEMPLATE_P_FACTORY(TYPE, auto, ESCAPE(T_ARG), ESCAPE(ARG))
 
 #endif //TEMPO_FACTORY_PATTERN_HPP
diff --git a/src/header/util/task_timings.hpp b/src/header/util/task_timings.hpp
deleted file mode 100644
index cf245e8637f0330f0380912546c8e061476d24cf..0000000000000000000000000000000000000000
--- a/src/header/util/task_timings.hpp
+++ /dev/null
@@ -1,97 +0,0 @@
-/**
- * @author Tim Luchterhand
- * @date 10.11.23
- */
-
-#ifndef TEMPO_TASK_TIMINGS_HPP
-#define TEMPO_TASK_TIMINGS_HPP
-#include "Global.hpp"
-#include "traits.hpp"
-
-namespace tempo {
-    /**
-     * Reads out the minimum task duration from an event network
-     * @tparam EvtFun type of event distance function
-     * @param t task id
-     * @param eventDistanceFunction event distance function from which to read out the timing values
-     * @return minimum task duration
-     */
-    template<concepts::arbitrary_event_dist_fun EvtFun>
-    constexpr auto minDuration(task t, const EvtFun &eventDistanceFunction) {
-        return -eventDistanceFunction(END(t), START(t));
-    }
-
-    /**
-     * Reads out the maximum task duration from an event network
-     * @tparam EvtFun type of event distance function
-     * @param t task id
-     * @param eventDistanceFunction event distance function from which to read out the timing values
-     * @return maximum task duration
-     */
-    template<concepts::arbitrary_event_dist_fun EvtFun>
-    constexpr auto maxDuration(task t, const EvtFun &eventDistanceFunction) {
-        return eventDistanceFunction(START(t), END(t));
-    }
-
-    /**
-     * Reads out the earliest starting time of a task from an event network
-     * @tparam EvtFun type of event distance function
-     * @param t task id
-     * @param eventDistanceFunction event distance function from which to read out the timing values
-     * @return earliest task starting time
-     */
-    template<concepts::arbitrary_event_dist_fun EvtFun>
-    constexpr auto earliestStartTime(task t, const EvtFun &eventDistanceFunction) {
-        return eventDistanceFunction(START(t), ORIGIN);
-    }
-
-    /**
-     * Reads out the latest deadline of a task from an event network
-     * @tparam EvtFun type of event distance function
-     * @param t task id
-     * @param eventDistanceFunction event distance function from which to read out the timing values
-     * @return latest task completion time
-     */
-    template<concepts::arbitrary_event_dist_fun EvtFun>
-    constexpr auto latestCompletion(task t, const EvtFun &eventDistanceFunction) {
-        return eventDistanceFunction(HORIZON, END(t));
-    }
-
-    /**
-     * Reads out the distance between two tasks from an event network, i.e. Let a and b be two tasks, then the distance
-     * between a and b is the distance between a's start event and b's end event
-     * @tparam EvtFun type of event distance function
-     * @param from id of the first task
-     * @param to id of the second task
-     * @param eventDistanceFunction event distance function from which to read out the timing values
-     * @return distance between START(a) and END(b)
-     */
-    template<concepts::arbitrary_event_dist_fun EvtFun>
-    constexpr auto taskDistance(task from, task to, const EvtFun &eventDistanceFunction) {
-        return eventDistanceFunction(START(from), END(to));
-    }
-
-    /**
-     * Reads out the upper bound from an event network, i.e. the distance between the origin and the horizon events
-     * @tparam EvtFun type of event distance function
-     * @param eventDistanceFunction event distance function from which to read out the timing values
-     * @return distance between ORIGIN and HORIZON
-     */
-    template<concepts::arbitrary_event_dist_fun EvtFun>
-    constexpr auto upperBound(const EvtFun &eventDistanceFunction) {
-        return eventDistanceFunction(ORIGIN, HORIZON);
-    }
-
-    /**
-     * Reads out the lower bound from an event network, i.e. the negative distance between the horizon and origin events
-     * @tparam EvtFun type of event distance function
-     * @param eventDistanceFunction event distance function from which to read out the timing values
-     * @return negative distance between HORIZON and ORIGIN
-     */
-    template<concepts::arbitrary_event_dist_fun EvtFun>
-    constexpr auto lowerBound(const EvtFun &eventDistanceFunction) {
-        return -eventDistanceFunction(HORIZON, ORIGIN);
-    }
-}
-
-#endif //TEMPO_TASK_TIMINGS_HPP
diff --git a/src/header/util/traits.hpp b/src/header/util/traits.hpp
index 96e71a82a349ef21555105ca8f2449badda2e711..c9ec5cadd5538dc6e82ae2838eec47af2a1d2ee1 100644
--- a/src/header/util/traits.hpp
+++ b/src/header/util/traits.hpp
@@ -74,12 +74,18 @@ namespace tempo::concepts {
     template<typename T>
     concept scalar = std::integral<T> || std::floating_point<T>;
 
-//    template<typename E, typename T>
-//    concept event_dist_fun = callable_r<E, T, event, event>;
-//
-//    template<typename E>
-//    concept arbitrary_event_dist_fun = std::invocable<E, event, event> &&
-//                                       scalar<std::remove_reference_t<std::invoke_result_t<E, event, event>>>;
+    template<typename E, typename T>
+    concept task_dist_fun = callable_r<E, T, unsigned , unsigned>;
+
+    template<typename E>
+    concept arbitrary_task_dist_fun = std::invocable<E, unsigned, unsigned> &&
+                                       scalar<std::remove_reference_t<std::invoke_result_t<E, unsigned, unsigned>>>;
+
+    template<typename S>
+    concept distance_provider = requires(const S instance, var_t e) {
+        { instance.numeric.upper(e) } -> scalar;
+        { instance.numeric.lower(e) } -> scalar;
+    };
 
     template<typename R, typename T>
     concept typed_range = std::ranges::range<R> && std::same_as<std::ranges::range_value_t<R>, T>;