diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index af987c2..d6f25dc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,7 +25,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [macOS-11, macOS-latest, windows-2022] + os: [macOS-11, macOS-12, macos-13, windows-2022] steps: - uses: actions/checkout@v3 @@ -51,7 +51,7 @@ jobs: deploy-pypi: runs-on: ubuntu-latest needs: [build-linux, build-win-and-mac] - if: github.repository == 'speckdavid/up-symk' # We only deploy on the correct repo (TODO: change the repo) + if: ${{ (github.repository == 'speckdavid/up-symk') && (github.ref == 'refs/heads/master') }} # We only deploy on the correct repo and branch steps: - uses: actions/download-artifact@master diff --git a/README.md b/README.md index d934eb0..376d666 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,15 @@ # Integration of SymK with the Unified Planning Library +This repository was created within the `Symbolic Search for Diverse Plans and Maximum Utility` project funded by the by the [AIPlan4EU project](https://www.aiplan4eu-project.eu/). +This project aims to enhance the [unified planning library](https://github.com/aiplan4eu/unified-planning) with four expressive extensions to traditional classical planning using the symbolic search planner [SymK](https://github.com/speckdavid/symk). + ## Installation +Currently, we are in the development phase. +We recommend building our version of the [unified planning library](https://github.com/speckdavid/unified-planning) locally and then building this `up-symk` package locally. -Currently we are in the development phase and everything has to be built locally. First, build locally our version of the [unified planning library](https://github.com/speckdavid/unified-planning) where we have registered SymK. +Please note that many core functionalities have already been merged into the official unified planning library repository, and the stable pypi version is available. You can install everything with `pip install unified-planning` and `pip install up-symk`. +However, it's important to be aware that the stable version 1.0.0 of unified-planning does not include all the necessary changes yet. +Therefore, we recommend following the installation steps below to proceed: The following should install all necessary dependencies. ``` @@ -24,4 +31,6 @@ pip install up-symk/ ``` ## Usages -In the [notebooks folder](notebooks/), you can find an [example](https://github.com/aiplan4eu/up-symk/blob/master/notebooks/symk_usage.ipynb) of how to use the SymK planner within the unified planning library. +In the [notebooks folder](notebooks/), you can find two examples of how to use the SymK planner within the unified planning library. + - [Multi-Solution Generation: Using SymK in the Unified Planning Library](https://github.com/aiplan4eu/up-symk/blob/master/notebooks/symk_usage.ipynb) + - [Optimizing Plan Utility: Using SymK in the Unified Planning Library](https://github.com/aiplan4eu/up-symk/blob/master/notebooks/symk_osp_usage.ipynb) diff --git a/notebooks/symk_osp_usage.ipynb b/notebooks/symk_osp_usage.ipynb new file mode 100644 index 0000000..e583117 --- /dev/null +++ b/notebooks/symk_osp_usage.ipynb @@ -0,0 +1,576 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "68dedba5", + "metadata": {}, + "source": [ + "# Optimizing Plan Utility: Using SymK in the Unified Planning Library" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "54204a66", + "metadata": {}, + "outputs": [], + "source": [ + "import networkx as nx\n", + "import matplotlib\n", + "import random\n", + "import sys\n", + "import time\n", + "import up_symk\n", + "import unified_planning as up\n", + "\n", + "from collections import defaultdict\n", + "from unified_planning.shortcuts import *\n", + "\n", + "seed = 1\n", + "random.seed(seed)" + ] + }, + { + "cell_type": "markdown", + "id": "6a93c0be", + "metadata": {}, + "source": [ + "## Validator\n", + "We define two functions that serve as helper functions to evaluate the found plans and calculate the cost of the plans." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "32b9b080", + "metadata": {}, + "outputs": [], + "source": [ + "def get_plan_qualities(problem, plan):\n", + " qualities = []\n", + " for qm in problem.quality_metrics:\n", + " new_problem = problem.clone()\n", + " new_problem.clear_quality_metrics()\n", + " new_problem.add_quality_metric(qm)\n", + " pv = PlanValidator(problem_kind=new_problem.kind)\n", + " pv_res = pv.validate(new_problem, plan)\n", + " qualities.append(pv_res.metric_evaluations[qm])\n", + " return qualities\n", + "\n", + "def get_plans_min_max_qualities(problem, plans):\n", + " qualities = []\n", + " for qm in problem.quality_metrics:\n", + " min_q = float('inf')\n", + " max_q = -float('inf')\n", + " new_problem = problem.clone()\n", + " new_problem.clear_quality_metrics()\n", + " new_problem.add_quality_metric(qm)\n", + " pv = PlanValidator(problem_kind=new_problem.kind)\n", + " for plan in plans:\n", + " pv_res = pv.validate(new_problem, plan)\n", + " cur_q = pv_res.metric_evaluations[qm]\n", + " min_q = min(min_q, cur_q)\n", + " max_q = max(max_q, cur_q)\n", + " qualities.append((min_q, max_q))\n", + " return qualities " + ] + }, + { + "cell_type": "markdown", + "id": "71cbaf15", + "metadata": {}, + "source": [ + "## A simple example planning task\n", + "To illustrate, we randomly generate a planning task where a single truck with no capacity limit has to deliver a number of parcels of different importance between locations on a graph. Note that the road network is modeled as a directed graph, and it is possible that not all parcels can be delivered." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "db40758b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Jobs:\n", + "Parcel 0 from location 1 to location 5 with imporantce 1\n", + "Parcel 1 from location 2 to location 0 with imporantce 7\n", + "Parcel 2 from location 3 to location 4 with imporantce 6\n", + "Parcel 3 from location 1 to location 0 with imporantce 7\n", + "Parcel 4 from location 0 to location 4 with imporantce 6\n", + "Parcel 5 from location 4 to location 0 with imporantce 7\n", + "Parcel 6 from location 2 to location 1 with imporantce 9\n", + "Parcel 7 from location 0 to location 3 with imporantce 0\n", + "Parcel 8 from location 0 to location 1 with imporantce 8\n", + "Parcel 9 from location 0 to location 4 with imporantce 3\n", + "Parcel 10 from location 3 to location 0 with imporantce 8\n", + "Parcel 11 from location 1 to location 4 with imporantce 7\n", + "The truck is at location 4\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def generate_problem(num_locations, num_parcels):\n", + " # generate a random connected graph with the locations \n", + " graph = nx.fast_gnp_random_graph(num_locations, 0.31, directed=True, seed=seed)\n", + "\n", + " # randomly choose the origin and destination of every parcel\n", + " jobs = []\n", + " for parcel in range(num_parcels):\n", + " origin = random.randrange(num_locations) \n", + " destination = random.choice([x for x in range(num_locations)\n", + " if x != origin])\n", + " importance = random.randrange(10)\n", + " jobs.append((origin, destination, importance))\n", + "\n", + "\n", + " # randlomly choose a truck location\n", + " truck_location = random.randrange(num_locations)\n", + " return graph, jobs, truck_location\n", + "\n", + "num_locations = 6\n", + "num_parcels = 12\n", + "graph, jobs, truck_location = generate_problem(num_locations, num_parcels)\n", + "\n", + "nx.draw_networkx(graph, with_labels=True, arrows=True, arrowsize=12) # uncomment if you do not have matplotlib\n", + "print(\"Jobs:\")\n", + "for parcel_id, (origin, destination, importance) in enumerate(jobs):\n", + " print('Parcel', parcel_id, 'from location', origin,\n", + " 'to location', destination, \"with imporantce\", importance) \n", + "print('The truck is at location', truck_location)" + ] + }, + { + "cell_type": "markdown", + "id": "ddf47eec", + "metadata": {}, + "source": [ + "The possible actions in the planning task are to move the truck from a location to a connected location, to load a parcel (if the truck is at the same location), and to unload the parcel at the destination location.\n", + "\n", + "We use a unary predicate ``truck_at`` and a binary predicate ``parcel_at`` to represent the locations of the truck and all the parcels. The unary predicates ``parcel_loaded`` and ``delivered`` express that a parcel is in the truck or has been delivered. The destinations of the parcels are represented by the binary predicate ``destination`` and the connections (edges) by the binary predicate ``connected``. \n", + "\n", + "The goal is oversubscribed in the sense that for each parcel we successfully deliver, we get the importance value of the parcel." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "23a8eefa", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Goals with utilities:\n", + "delivered(parcel0) 1\n", + "delivered(parcel1) 7\n", + "delivered(parcel2) 6\n", + "delivered(parcel3) 7\n", + "delivered(parcel4) 6\n", + "delivered(parcel5) 7\n", + "delivered(parcel6) 9\n", + "delivered(parcel7) 0\n", + "delivered(parcel8) 8\n", + "delivered(parcel9) 3\n", + "delivered(parcel10) 8\n", + "delivered(parcel11) 7\n" + ] + } + ], + "source": [ + "def get_planning_task(graph, jobs, truck_location):\n", + " num_parcels = len(jobs)\n", + " num_locations = len(graph)\n", + " # declare user types\n", + " Location = UserType('Location')\n", + " Parcel = UserType('Parcel')\n", + "\n", + " # declare predicates\n", + " truck_at = up.model.Fluent('truck_at', BoolType(), l=Location)\n", + " parcel_at = up.model.Fluent('parcel_at', BoolType(), p=Parcel, l=Location)\n", + " parcel_loaded = up.model.Fluent('parcel_loaded', BoolType(), p=Parcel)\n", + " delivered = up.model.Fluent('delivered', BoolType(), p=Parcel)\n", + " destination = up.model.Fluent('destination', BoolType(), p=Parcel, l=Location)\n", + " connected = up.model.Fluent('connected', BoolType(), l_from=Location, l_to=Location)\n", + "\n", + " # add (typed) objects to problem\n", + " problem = up.model.Problem('parcels')\n", + " locations = [up.model.Object('loc%s' % i, Location)\n", + " for i in range(num_locations)]\n", + " parcels = [up.model.Object('parcel%s' % i, Parcel)\n", + " for i in range(num_parcels)]\n", + " problem.add_objects(locations)\n", + " problem.add_objects(parcels)\n", + "\n", + " # specify the initial state\n", + " problem.add_fluent(truck_at, default_initial_value=False)\n", + " problem.add_fluent(parcel_at, default_initial_value=False)\n", + " problem.add_fluent(parcel_loaded, default_initial_value=False)\n", + " problem.add_fluent(delivered, default_initial_value=False)\n", + " problem.add_fluent(destination, default_initial_value=False)\n", + " problem.add_fluent(connected, default_initial_value=False)\n", + " for parcel_id, (origin, dest, _) in enumerate(jobs):\n", + " p = parcels[parcel_id]\n", + " problem.set_initial_value(parcel_at(p, locations[origin]), True)\n", + " problem.set_initial_value(destination(p, locations[dest]), True)\n", + " for (l1, l2) in graph.edges():\n", + " problem.set_initial_value(connected(locations[l1], locations[l2]), True)\n", + " # problem.set_initial_value(connected(locations[l2], locations[l1]), True)\n", + " problem.set_initial_value(truck_at(locations[truck_location]), True)\n", + "\n", + " # add actions\n", + " move = up.model.InstantaneousAction('move', l_from=Location, l_to=Location)\n", + " l_from = move.parameter('l_from')\n", + " l_to = move.parameter('l_to')\n", + " move.add_precondition(connected(l_from, l_to))\n", + " move.add_precondition(truck_at(l_from))\n", + " move.add_effect(truck_at(l_from), False)\n", + " move.add_effect(truck_at(l_to), True)\n", + " problem.add_action(move)\n", + "\n", + " load = up.model.InstantaneousAction('load', p=Parcel, l=Location)\n", + " p = load.parameter('p')\n", + " loc = load.parameter('l')\n", + " load.add_precondition(truck_at(loc))\n", + " load.add_precondition(parcel_at(p, loc))\n", + " load.add_effect(parcel_at(p, loc), False)\n", + " load.add_effect(parcel_loaded(p), True)\n", + " problem.add_action(load)\n", + "\n", + " unload = up.model.InstantaneousAction('unload', p=Parcel, l=Location)\n", + " p = unload.parameter('p')\n", + " loc = unload.parameter('l')\n", + " unload.add_precondition(truck_at(loc))\n", + " unload.add_precondition(parcel_loaded(p))\n", + " unload.add_precondition(destination(p, loc))\n", + " unload.add_effect(delivered(p), True)\n", + " unload.add_effect(parcel_loaded(p), False)\n", + " problem.add_action(unload)\n", + " \n", + " # specify the goal: all parcels should have been delivered\n", + " goals = {}\n", + " for parcel_id, (_, _, importance) in enumerate(jobs):\n", + " p = parcels[parcel_id]\n", + " goals[delivered(p)] = importance\n", + " \n", + " problem.add_quality_metric(up.model.metrics.Oversubscription(goals))\n", + " \n", + " return problem\n", + "\n", + "problem = get_planning_task(graph, jobs, truck_location)\n", + "print(\"Goals with utilities:\")\n", + "for g, u in problem.quality_metrics[0].goals.items():\n", + " print(g, u)" + ] + }, + { + "cell_type": "markdown", + "id": "0ab4f15e", + "metadata": {}, + "source": [ + "## Finding a single optimal solution\n", + "We solve our problem optimally with ``symk-opt``, i.e., we find a plan with the highest possible utility." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8d28bee7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96m\u001b[1mNOTE: To disable printing of planning engine credits, add this line to your code: `up.shortcuts.get_environment().credits_stream = None`\n", + "\u001b[0m\u001b[96m *** Credits ***\n", + "\u001b[0m\u001b[96m * In operation mode `OneshotPlanner` at line 1 of `/tmp/ipykernel_23785/1197866876.py`, \u001b[0m\u001b[96myou are using the following planning engine:\n", + "\u001b[0m\u001b[96m * Engine name: SymK\n", + " * Developers: David Speck (cf. https://github.com/speckdavid/symk/blob/master/README.md )\n", + "\u001b[0m\u001b[96m * Description: \u001b[0m\u001b[96mSymK is a state-of-the-art domain-independent optimal and top-k planner.\u001b[0m\u001b[96m\n", + "\u001b[0m\u001b[96m\n", + "\u001b[0mSymK (with optimality guarantee) returned: SequentialPlan:\n", + " move(loc4, loc3)\n", + " load(parcel2, loc3)\n", + " move(loc3, loc5)\n", + " move(loc5, loc4)\n", + " unload(parcel2, loc4)\n", + " move(loc4, loc2)\n", + " load(parcel6, loc2)\n", + " move(loc2, loc1)\n", + " unload(parcel6, loc1)\n", + " load(parcel11, loc1)\n", + " load(parcel0, loc1)\n", + " move(loc1, loc5)\n", + " unload(parcel0, loc5)\n", + " move(loc5, loc4)\n", + " unload(parcel11, loc4) with a utility of 23 in 0.246795 seconds.\n" + ] + } + ], + "source": [ + "with OneshotPlanner(name=\"symk-opt\") as planner:\n", + " start_time = time.time()\n", + " result = planner.solve(problem)\n", + " execution_time = time.time() - start_time \n", + " plan = result.plan\n", + " assert plan is not None\n", + " qualities = get_plan_qualities(problem, plan)\n", + " assert len(qualities) == 1\n", + " utility = qualities[0]\n", + " print(f\"{planner.name} returned: {result.plan} with a utility of {utility} in {execution_time:.6f} seconds.\")\n", + " \n", + "# We do not want to see the credits again and again, so let's disable them from now on.\n", + "up.shortcuts.get_environment().credits_stream = None" + ] + }, + { + "cell_type": "markdown", + "id": "1fb01403", + "metadata": {}, + "source": [ + "We can also use the ``oversubscription`` meta-engine (here with ``tamer``). However, this has some disadvantages compared to using ``symk-opt`` or ``symk``, which supports oversubscription tasks natively. The meta-engine employs a compilation technique, generating a planning task for the powerset of all goals, which can result in significant exponential overhead, as evident to some extent in this example. Furthermore, planning engines based on Fast Downward may not be able to handle the negated goal facts introduced by the compilation. We recommend the use of ``symk-opt`` or ``symk``." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "29752b7d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Defaulting to user installation because normal site-packages is not writeable\n", + "Requirement already satisfied: up_tamer in /home/david/.local/lib/python3.10/site-packages (0.3.1.22.dev1)\n", + "Requirement already satisfied: pytamer==0.1.10 in /home/david/.local/lib/python3.10/site-packages (from up_tamer) (0.1.10)\n", + "OversubscriptionPlanner[Tamer] returned: SequentialPlan:\n", + " move(loc4, loc1)\n", + " load(parcel0, loc1)\n", + " load(parcel11, loc1)\n", + " move(loc1, loc5)\n", + " unload(parcel0, loc5)\n", + " move(loc5, loc4)\n", + " unload(parcel11, loc4)\n", + " move(loc4, loc2)\n", + " load(parcel6, loc2)\n", + " move(loc2, loc1)\n", + " unload(parcel6, loc1)\n", + " move(loc1, loc5)\n", + " move(loc5, loc4)\n", + " move(loc4, loc3)\n", + " load(parcel2, loc3)\n", + " move(loc3, loc5)\n", + " move(loc5, loc4)\n", + " unload(parcel2, loc4) with a utility of 23 in 45.610930 seconds.\n" + ] + } + ], + "source": [ + "!pip install up_tamer\n", + "\n", + "with OneshotPlanner(name=\"oversubscription[tamer]\") as planner:\n", + " start_time = time.time()\n", + " result = planner.solve(problem)\n", + " execution_time = time.time() - start_time \n", + " plan = result.plan\n", + " assert plan is not None\n", + " qualities = get_plan_qualities(problem, plan)\n", + " assert len(qualities) == 1\n", + " utility = qualities[0]\n", + " print(f\"{planner.name} returned: {result.plan} with a utility of {utility} in {execution_time:.6f} seconds.\")" + ] + }, + { + "cell_type": "markdown", + "id": "be3b9356", + "metadata": {}, + "source": [ + "## Finding a single optimal solution for a given cost bound\n", + "With ``symk-opt`` it is possible to find the best solution, i.e., the solution that maximizes the utility of the end state with respect to a given cost bound (here: plan length) that can be specified as an additional parameter. Here we ask for a solution that maximizes utility and has a cost lower than 7." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "2d93cba3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SymK (with optimality guarantee) returned: SequentialPlan:\n", + " move(loc4, loc2)\n", + " load(parcel6, loc2)\n", + " move(loc2, loc1)\n", + " unload(parcel6, loc1) with a utility of 9 and plan length of 4 in 0.238176 seconds.\n" + ] + } + ], + "source": [ + "if MinimizeSequentialPlanLength() not in problem.quality_metrics:\n", + " problem.add_quality_metric(MinimizeSequentialPlanLength())\n", + "\n", + "with OneshotPlanner(name=\"symk-opt\", params={\"plan_cost_bound\": 7}) as planner:\n", + " start_time = time.time()\n", + " result = planner.solve(problem)\n", + " execution_time = time.time() - start_time \n", + " plan = result.plan\n", + " assert plan is not None\n", + " qualities = get_plan_qualities(problem, plan)\n", + " assert len(qualities) == 2\n", + " utility, plan_length = qualities\n", + " print(f\"{planner.name} returned: {result.plan} with a utility of {utility} and plan length of {plan_length} in {execution_time:.6f} seconds.\")" + ] + }, + { + "cell_type": "markdown", + "id": "74a7c96f", + "metadata": {}, + "source": [ + "## Finding multiple optimal solutions\n", + "We solve the problem with ``symk-opt`` and ask for all optimal solutions, i.e., all solutions that maximize utility at potentially different costs (here: plan length)." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "68bdf52e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SymK (with optimality guarantee) found 100 plans...\n", + "SymK (with optimality guarantee) found 200 plans...\n", + "SymK (with optimality guarantee) found 300 plans...\n", + "SymK (with optimality guarantee) found 400 plans...\n", + "SymK (with optimality guarantee) found 500 plans...\n", + "SymK (with optimality guarantee) found 600 plans...\n", + "SymK (with optimality guarantee) found 700 plans...\n", + "SymK (with optimality guarantee) found 800 plans...\n", + "\n", + "SymK (with optimality guarantee) found 858 plans!\n", + "\n", + "Calculate the utlities and costs of the plans found...\n", + "SymK (with optimality guarantee) found 858 plans with utility 23 and costs between 15 and 18.\n" + ] + } + ], + "source": [ + "plans = []\n", + "\n", + "with AnytimePlanner(name='symk-opt') as planner:\n", + " for i, result in enumerate(planner.get_solutions(problem)):\n", + " if result.status == up.engines.PlanGenerationResultStatus.INTERMEDIATE:\n", + " plans.append(result.plan)\n", + " if i > 0 and i % 100 == 0:\n", + " print(f\"{planner.name} found {i} plans...\")\n", + " elif result.status in unified_planning.engines.results.POSITIVE_OUTCOMES:\n", + " print()\n", + " print(f\"{planner.name} found {i} plans!\")\n", + " print()\n", + " elif result.status not in unified_planning.engines.results.POSITIVE_OUTCOMES:\n", + " print(\"No plan found.\")\n", + " \n", + "# Calculate the utlities and costs of the plans found\n", + "print(\"Calculate the utlities and costs of the plans found...\")\n", + "\n", + "utils, costs = get_plans_min_max_qualities(problem, plans)\n", + "assert utils[0] == utils[1]\n", + " \n", + "print(f\"{planner.name} found {len(plans)} plans with utility {utils[0]} and costs between {costs[0]} and {costs[1]}.\")" + ] + }, + { + "cell_type": "markdown", + "id": "49c5cf78", + "metadata": {}, + "source": [ + "## Find multiple solutions ordered by utility\n", + "Again we solve our problem, this time asking for 250 plans with cost less than 10. Our ``symk`` engine produces the solutions ordered by utility and then by cost (here: plan length), starting with the best ones, so that we find solutions that vary in utility and cost." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "cb26c497", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SymK found 100 plans...\n", + "SymK found 200 plans...\n", + "\n", + "SymK found 250 plans!\n", + "\n", + "Calculate the utlities and costs of the plans found...\n", + "SymK found 250 plans with utilities between 16 and 1 and costs between 4 and 9.\n" + ] + } + ], + "source": [ + "plans = []\n", + "\n", + "with AnytimePlanner(name='symk', params={\"plan_cost_bound\": 10, \"number_of_plans\": 250}) as planner:\n", + " for i, result in enumerate(planner.get_solutions(problem)):\n", + " if result.status == up.engines.PlanGenerationResultStatus.INTERMEDIATE:\n", + " plans.append(result.plan)\n", + " if i > 0 and i % 100 == 0:\n", + " print(f\"{planner.name} found {i} plans...\")\n", + " elif result.status in unified_planning.engines.results.POSITIVE_OUTCOMES:\n", + " print()\n", + " print(f\"{planner.name} found {i} plans!\")\n", + " print()\n", + " elif result.status not in unified_planning.engines.results.POSITIVE_OUTCOMES:\n", + " print(\"No plan found.\")\n", + " \n", + "# Calculate the utlities and costs of the plans found\n", + "print(\"Calculate the utlities and costs of the plans found...\")\n", + "\n", + "utils, costs = get_plans_min_max_qualities(problem, plans)\n", + " \n", + "print(f\"{planner.name} found {len(plans)} plans with utilities between {utils[1]} and {utils[0]} and costs between {costs[0]} and {costs[1]}.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "aiplan4eu3.10", + "language": "python", + "name": "aiplan4eu3.10" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/symk_usage.ipynb b/notebooks/symk_usage.ipynb index 3f5beff..04a26be 100644 --- a/notebooks/symk_usage.ipynb +++ b/notebooks/symk_usage.ipynb @@ -1,5 +1,13 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "e3cf0883", + "metadata": {}, + "source": [ + "# Multi-Solution Generation: Using SymK in the Unified Planning Library" + ] + }, { "cell_type": "code", "execution_count": 1, @@ -16,6 +24,16 @@ "from unified_planning.io import PDDLReader" ] }, + { + "cell_type": "markdown", + "id": "7b6ae544", + "metadata": {}, + "source": [ + "## A simple example\n", + "\n", + "We use the iconic problem number one of the gripper domain, where the goal is to move four balls from room A to room B with a gripper that has two arms, which we read in as pddl files." + ] + }, { "cell_type": "code", "execution_count": 2, @@ -23,19 +41,54 @@ "metadata": {}, "outputs": [], "source": [ - "# Read a simple PDDL gripper problem from file\n", - "\n", "reader = PDDLReader()\n", "pddl_problem = reader.parse_problem(\"gripper-domain.pddl\", \"gripper-prob01.pddl\")\n", - "pddl_problem.add_quality_metric(MinimizeSequentialPlanLength())\n", - "\n", - "# Validator\n", - "pv = PlanValidator(problem_kind=pddl_problem.kind)" + "pddl_problem.add_quality_metric(MinimizeSequentialPlanLength())" + ] + }, + { + "cell_type": "markdown", + "id": "7ab964b3", + "metadata": {}, + "source": [ + "## Validator\n", + "We define two functions that serve as helper functions to evaluate the found plans and calculate the cost of the plans." ] }, { "cell_type": "code", "execution_count": 3, + "id": "9f025ef9", + "metadata": {}, + "outputs": [], + "source": [ + "def get_plan_cost(problem, plan):\n", + " pv = PlanValidator(problem_kind=problem.kind)\n", + " pv_res = pv.validate(problem, plan)\n", + " return pv_res.metric_evaluations[problem.quality_metrics[0]]\n", + "\n", + "def get_plans_by_cost(problem, plans):\n", + " pv = PlanValidator(problem_kind=problem.kind)\n", + " plans_by_cost = defaultdict(lambda: [])\n", + " for plan in plans:\n", + " pv_res = pv.validate(problem, plan)\n", + " cost = pv_res.metric_evaluations[problem.quality_metrics[0]]\n", + " plans_by_cost[cost].append(result.plan)\n", + " return plans_by_cost" + ] + }, + { + "cell_type": "markdown", + "id": "fb03e2ef", + "metadata": {}, + "source": [ + "## Finding a single optimal solution\n", + "We solve our gripper problem optimally with ``symk-opt``." + ] + }, + { + "cell_type": "code", + "execution_count": 4, "id": "23a8eefa", "metadata": {}, "outputs": [ @@ -45,17 +98,17 @@ "text": [ "\u001b[96m\u001b[1mNOTE: To disable printing of planning engine credits, add this line to your code: `up.shortcuts.get_environment().credits_stream = None`\n", "\u001b[0m\u001b[96m *** Credits ***\n", - "\u001b[0m\u001b[96m * In operation mode `OneshotPlanner` at line 3 of `/tmp/ipykernel_79480/4014978636.py`, \u001b[0m\u001b[96myou are using the following planning engine:\n", + "\u001b[0m\u001b[96m * In operation mode `OneshotPlanner` at line 1 of `/tmp/ipykernel_23488/3917870263.py`, \u001b[0m\u001b[96myou are using the following planning engine:\n", "\u001b[0m\u001b[96m * Engine name: SymK\n", " * Developers: David Speck (cf. https://github.com/speckdavid/symk/blob/master/README.md )\n", - "\u001b[0m\u001b[96m * Description: \u001b[0m\u001b[96mSymK is a state-of-the-art domain-independent classical optimal and top-k planner.\u001b[0m\u001b[96m\n", + "\u001b[0m\u001b[96m * Description: \u001b[0m\u001b[96mSymK is a state-of-the-art domain-independent optimal and top-k planner.\u001b[0m\u001b[96m\n", "\u001b[0m\u001b[96m\n", - "\u001b[0mSymK found this plan: SequentialPlan:\n", + "\u001b[0mSymK (with optimality guarantee) found this plan: SequentialPlan:\n", " pick(ball2, rooma, right)\n", " pick(ball1, rooma, left)\n", " move(rooma, roomb)\n", - " drop(ball1, roomb, left)\n", " drop(ball2, roomb, right)\n", + " drop(ball1, roomb, left)\n", " move(roomb, rooma)\n", " pick(ball3, rooma, left)\n", " pick(ball4, rooma, right)\n", @@ -66,21 +119,30 @@ } ], "source": [ - "# Solve problem optimally with SymK (bidirectional symbolic search)\n", - "\n", - "with OneshotPlanner(name='symk') as planner:\n", + "with OneshotPlanner(name='symk-opt') as planner:\n", " result = planner.solve(pddl_problem) # output_stream=sys.stdout\n", " if result.status in unified_planning.engines.results.POSITIVE_OUTCOMES:\n", - " pv_res = pv.validate(pddl_problem, result.plan)\n", - " cost = pv_res.metric_evaluations[pddl_problem.quality_metrics[0]]\n", + " cost = get_plan_cost(pddl_problem, result.plan)\n", " print(f\"{planner.name} found this plan: {result.plan} with cost {cost}.\")\n", " else:\n", - " print(\"No plan found.\")" + " print(\"No plan found.\")\n", + " \n", + "# We do not want to see the credits again and again, so let's disable them from now on.\n", + "up.shortcuts.get_environment().credits_stream = None" + ] + }, + { + "cell_type": "markdown", + "id": "466266be", + "metadata": {}, + "source": [ + "## Finding multiple optimal solutions\n", + "We solve our gripper problem with ``symk-opt`` and ask for three optimal solutions, which are reported." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "8d28bee7", "metadata": {}, "outputs": [ @@ -88,13 +150,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "\u001b[96m *** Credits ***\n", - "\u001b[0m\u001b[96m * In operation mode `AnytimePlanner` at line 5 of `/tmp/ipykernel_79480/3044654666.py`, \u001b[0m\u001b[96myou are using the following planning engine:\n", - "\u001b[0m\u001b[96m * Engine name: SymK\n", - " * Developers: David Speck (cf. https://github.com/speckdavid/symk/blob/master/README.md )\n", - "\u001b[0m\u001b[96m * Description: \u001b[0m\u001b[96mSymK is a state-of-the-art domain-independent classical optimal and top-k planner.\u001b[0m\u001b[96m\n", - "\u001b[0m\u001b[96m\n", - "\u001b[0mPlan 1: SequentialPlan:\n", + "Plan 1: SequentialPlan:\n", " pick(ball2, rooma, right)\n", " pick(ball1, rooma, left)\n", " move(rooma, roomb)\n", @@ -133,60 +189,38 @@ " drop(ball3, roomb, right)\n", " drop(ball4, roomb, left) with cost 11.\n", "\n", - "Plan 4: SequentialPlan:\n", - " pick(ball2, rooma, right)\n", - " pick(ball1, rooma, left)\n", - " move(rooma, roomb)\n", - " drop(ball2, roomb, right)\n", - " drop(ball1, roomb, left)\n", - " move(roomb, rooma)\n", - " pick(ball3, rooma, right)\n", - " pick(ball4, rooma, left)\n", - " move(rooma, roomb)\n", - " drop(ball4, roomb, left)\n", - " drop(ball3, roomb, right) with cost 11.\n", - "\n", - "Plan 5: SequentialPlan:\n", - " pick(ball2, rooma, right)\n", - " pick(ball1, rooma, left)\n", - " move(rooma, roomb)\n", - " drop(ball2, roomb, right)\n", - " drop(ball1, roomb, left)\n", - " move(roomb, rooma)\n", - " pick(ball4, rooma, right)\n", - " pick(ball3, rooma, left)\n", - " move(rooma, roomb)\n", - " drop(ball3, roomb, left)\n", - " drop(ball4, roomb, right) with cost 11.\n", - "\n", - "SymK (with optimality guarantee) found 5 optimal plans with cost 11.\n" + "SymK (with optimality guarantee) found 3 optimal plans with cost 11.\n" ] } ], "source": [ - "# Find 5 optimal plans with SymK\n", - "\n", - "plans_by_cost = defaultdict(lambda: [])\n", + "plans = []\n", "\n", - "with AnytimePlanner(name='symk-opt', params={\"number_of_plans\": 5}) as planner:\n", + "with AnytimePlanner(name='symk-opt', params={\"number_of_plans\": 3}) as planner:\n", " for i, result in enumerate(planner.get_solutions(pddl_problem)): # output_stream=sys.stdout): \n", " if result.status == up.engines.PlanGenerationResultStatus.INTERMEDIATE:\n", - " pv_res = pv.validate(pddl_problem, result.plan)\n", - " cost = pv_res.metric_evaluations[pddl_problem.quality_metrics[0]]\n", - " plans_by_cost[cost].append(result.plan)\n", - " print(f\"Plan {i+1}: {result.plan} with cost {cost}.\")\n", + " plans.append(result.plan)\n", + " print(f\"Plan {i+1}: {result.plan} with cost {get_plan_cost(pddl_problem, result.plan)}.\")\n", " print()\n", " elif result.status in unified_planning.engines.results.POSITIVE_OUTCOMES:\n", - " assert len(plans_by_cost) == 1\n", - " for cost, plans in plans_by_cost.items():\n", + " for cost, plans in get_plans_by_cost(pddl_problem, plans).items():\n", " print(f\"{planner.name} found {len(plans)} optimal plans with cost {cost}.\")\n", " elif result.status not in unified_planning.engines.results.POSITIVE_OUTCOMES:\n", " print(\"No plan found.\") " ] }, + { + "cell_type": "markdown", + "id": "a49bd090", + "metadata": {}, + "source": [ + "## Finding all optimal solutions\n", + "We find all optimal solutions (using ``symk-opt``) and report the number of existing optimal plans and their cost." + ] + }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "69c01782", "metadata": {}, "outputs": [ @@ -194,45 +228,53 @@ "name": "stdout", "output_type": "stream", "text": [ - "\u001b[96m *** Credits ***\n", - "\u001b[0m\u001b[96m * In operation mode `AnytimePlanner` at line 5 of `/tmp/ipykernel_79480/3535417708.py`, \u001b[0m\u001b[96myou are using the following planning engine:\n", - "\u001b[0m\u001b[96m * Engine name: SymK\n", - " * Developers: David Speck (cf. https://github.com/speckdavid/symk/blob/master/README.md )\n", - "\u001b[0m\u001b[96m * Description: \u001b[0m\u001b[96mSymK is a state-of-the-art domain-independent classical optimal and top-k planner.\u001b[0m\u001b[96m\n", - "\u001b[0m\u001b[96m\n", - "\u001b[0mSymK (with optimality guarantee)\n", + "SymK (with optimality guarantee)\n", "SymK (with optimality guarantee) found 100 plans...\n", "SymK (with optimality guarantee) found 200 plans...\n", "SymK (with optimality guarantee) found 300 plans...\n", - "SymK (with optimality guarantee) found 384 optimal plans with cost 11.\n" + "\n", + "SymK (with optimality guarantee) found 384 plans!\n", + "\n", + "Calculate the cost of the plans found...\n", + "SymK (with optimality guarantee) found 384 plans with cost 11.\n" ] } ], "source": [ - "# Query an anytime planner with OPTIMAL_PLANS guarantee and generate all optimal plans => SymK\n", - "\n", - "plans_by_cost = defaultdict(lambda: [])\n", + "plans = []\n", "\n", "with AnytimePlanner(problem_kind=pddl_problem.kind, anytime_guarantee=\"OPTIMAL_PLANS\") as planner:\n", " print(planner.name)\n", " for i, result in enumerate(planner.get_solutions(pddl_problem)): # output_stream=sys.stdout): \n", " if result.status == up.engines.PlanGenerationResultStatus.INTERMEDIATE:\n", - " pv_res = pv.validate(pddl_problem, result.plan)\n", - " cost = pv_res.metric_evaluations[pddl_problem.quality_metrics[0]]\n", - " plans_by_cost[cost].append(result.plan)\n", + " plans.append(result.plan)\n", " if i > 0 and i % 100 == 0:\n", " print(f\"{planner.name} found {i} plans...\")\n", " elif result.status in unified_planning.engines.results.POSITIVE_OUTCOMES:\n", - " assert len(plans_by_cost) == 1\n", - " for cost, plans in plans_by_cost.items():\n", - " print(f\"{planner.name} found {len(plans)} optimal plans with cost {cost}.\")\n", + " print()\n", + " print(f\"{planner.name} found {i} plans!\")\n", + " print()\n", " elif result.status not in unified_planning.engines.results.POSITIVE_OUTCOMES:\n", - " print(\"No plan found.\") " + " print(\"No plan found.\")\n", + " \n", + "# Calculate the cost of the plans found\n", + "print(\"Calculate the cost of the plans found...\")\n", + "for cost, plans in get_plans_by_cost(pddl_problem, plans).items():\n", + " print(f\"{planner.name} found {len(plans)} plans with cost {cost}.\")" + ] + }, + { + "cell_type": "markdown", + "id": "6c175250", + "metadata": {}, + "source": [ + "## Finding multiple solutions ordered by cost\n", + "Again, we solve our grab problem and this time we ask for 500 plans, of which 384 plans are optimal with cost 11 and the remaining 116 of the 500 plans have cost 12." ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "253b5799", "metadata": {}, "outputs": [ @@ -240,18 +282,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "\u001b[96m *** Credits ***\n", - "\u001b[0m\u001b[96m * In operation mode `AnytimePlanner` at line 5 of `/tmp/ipykernel_79480/685317883.py`, \u001b[0m\u001b[96myou are using the following planning engine:\n", - "\u001b[0m\u001b[96m * Engine name: SymK\n", - " * Developers: David Speck (cf. https://github.com/speckdavid/symk/blob/master/README.md )\n", - "\u001b[0m\u001b[96m * Description: \u001b[0m\u001b[96mSymK is a state-of-the-art domain-independent classical optimal and top-k planner.\u001b[0m\u001b[96m\n", - "\u001b[0m\u001b[96m\n", - "\u001b[0mSymK found 100 plans...\n", + "SymK found 100 plans...\n", "SymK found 200 plans...\n", "SymK found 300 plans...\n", "SymK found 400 plans...\n", "\n", "SymK found 500 plans!\n", + "\n", + "Calculate the cost of the plans found...\n", "SymK found 384 plans with cost 11.\n", "SymK found 116 plans with cost 12.\n" ] @@ -259,24 +297,25 @@ ], "source": [ "# Find 500 plans with SymK\n", - "\n", - "plans_by_cost = defaultdict(lambda: [])\n", + "plans = []\n", "\n", "with AnytimePlanner(name='symk', params={\"number_of_plans\": 500}) as planner:\n", " for i, result in enumerate(planner.get_solutions(pddl_problem)): # output_stream=sys.stdout): \n", " if result.status == up.engines.PlanGenerationResultStatus.INTERMEDIATE:\n", - " pv_res = pv.validate(pddl_problem, result.plan)\n", - " cost = pv_res.metric_evaluations[pddl_problem.quality_metrics[0]]\n", - " plans_by_cost[cost].append(result.plan)\n", + " plans.append(result.plan)\n", " if i > 0 and i % 100 == 0:\n", " print(f\"{planner.name} found {i} plans...\")\n", " elif result.status in unified_planning.engines.results.POSITIVE_OUTCOMES:\n", " print()\n", " print(f\"{planner.name} found {i} plans!\")\n", - " for cost, plans in plans_by_cost.items():\n", - " print(f\"{planner.name} found {len(plans)} plans with cost {cost}.\")\n", + " print()\n", " elif result.status not in unified_planning.engines.results.POSITIVE_OUTCOMES:\n", - " print(\"No plan found.\") " + " print(\"No plan found.\")\n", + "\n", + "# Calculate the cost of the plans found\n", + "print(\"Calculate the cost of the plans found...\")\n", + "for cost, plans in get_plans_by_cost(pddl_problem, plans).items():\n", + " print(f\"{planner.name} found {len(plans)} plans with cost {cost}.\")" ] } ], diff --git a/setup.py b/setup.py index 1347d3e..78dd2a8 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,6 @@ from wheel.bdist_wheel import bdist_wheel as _bdist_wheel class bdist_wheel(_bdist_wheel): - def finalize_options(self): _bdist_wheel.finalize_options(self) # Mark us as not a pure python package @@ -21,37 +20,39 @@ def finalize_options(self): def get_tag(self): python, abi, plat = _bdist_wheel.get_tag(self) # We don't link with python ABI, but require python3 - python, abi = 'py3', 'none' + python, abi = "py3", "none" return python, abi, plat + except ImportError: bdist_wheel = None -SYMK_REPO = 'https://github.com/speckdavid/symk.git' +SYMK_REPO = "https://github.com/speckdavid/symk.git" # SYMK_RELEASE = 'release-22.12' SYMK_RELEASE = None # CHANGESET is ignored if release is not None -SYMK_CHANGESET = '5b1ac056eb6129e3aadb1d44b1d5653bdbce59c5' +SYMK_CHANGESET = "5b1ac056eb6129e3aadb1d44b1d5653bdbce59c5" +SYMK_PATCH_NAME = "osp_patch_file.patch" def clone_and_compile_symk(): curr_dir = os.getcwd() print("Cloning SymK repository...") if SYMK_RELEASE is not None: - subprocess.run(['git', 'clone', '-b', SYMK_RELEASE, SYMK_REPO]) + subprocess.run(["git", "clone", "-b", SYMK_RELEASE, SYMK_REPO]) else: - subprocess.run(['git', 'clone', SYMK_REPO]) + subprocess.run(["git", "clone", SYMK_REPO]) - shutil.move('symk', 'up_symk/symk') - os.chdir('up_symk/symk') + shutil.move("symk", "up_symk/symk") + os.chdir("up_symk/symk") if SYMK_RELEASE is None: - subprocess.run(['git', 'checkout', SYMK_CHANGESET]) + subprocess.run(["git", "checkout", SYMK_CHANGESET]) + print("Applying patch...") + subprocess.run(["git", "apply", os.path.join("..", SYMK_PATCH_NAME)]) print("Building SymK (this can take some time)...") - build = subprocess.run([sys.executable, 'build.py']) - strip_downward = subprocess.run( - ['strip', '--strip-all', 'builds/release/bin/downward']) - strip_preprocess = subprocess.run( - ['strip', '--strip-all', 'builds/release/bin/preprocess']) + subprocess.run([sys.executable, "build.py"]) + subprocess.run(["strip", "--strip-all", "builds/release/bin/downward"]) + subprocess.run(["strip", "--strip-all", "builds/release/bin/preprocess"]) os.chdir(curr_dir) @@ -73,37 +74,40 @@ def run(self): long_description = "This package makes the [SymK](https://github.com/speckdavid/symk) planner available in the [unified_planning library](https://github.com/aiplan4eu/unified-planning) by the [AIPlan4EU project](https://www.aiplan4eu-project.eu/)." -setup(name='up_symk', - version='0.0.4', - description='Unified Planning Integration of the SymK planner', - long_description=long_description, - long_description_content_type="text/markdown", - author='David Speck', - author_email='david.speck@liu.se', - url='https://github.com/aiplan4eu/symk/', - classifiers=['Development Status :: 4 - Beta', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Programming Language :: Python :: 3', - 'Topic :: Scientific/Engineering :: Artificial Intelligence', - ], - packages=['up_symk'], - package_data={ - "": ['fast_downward.py', - 'symk/fast-downward.py', - 'symk/README.md', - 'symk/LICENSE.md', - 'symk/builds/release/bin/*', - 'symk/builds/release/bin/translate/*', - 'symk/builds/release/bin/translate/pddl/*', - 'symk/builds/release/bin/translate/pddl_parser/*', - 'symk/driver/*', - 'symk/driver/portfolios/*' - ] - }, - cmdclass={ - 'bdist_wheel': bdist_wheel, - 'build_py': install_symk, - 'develop': install_symk, - }, - has_ext_modules=lambda: True - ) +setup( + name="up_symk", + version="0.0.5", + description="Unified Planning Integration of the SymK planner", + long_description=long_description, + long_description_content_type="text/markdown", + author="David Speck", + author_email="david.speck@liu.se", + url="https://github.com/aiplan4eu/symk/", + classifiers=[ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + ], + packages=["up_symk"], + package_data={ + "": [ + "fast_downward.py", + "symk/fast-downward.py", + "symk/README.md", + "symk/LICENSE.md", + "symk/builds/release/bin/*", + "symk/builds/release/bin/translate/*", + "symk/builds/release/bin/translate/pddl/*", + "symk/builds/release/bin/translate/pddl_parser/*", + "symk/driver/*", + "symk/driver/portfolios/*", + ] + }, + cmdclass={ + "bdist_wheel": bdist_wheel, + "build_py": install_symk, + "develop": install_symk, + }, + has_ext_modules=lambda: True, +) diff --git a/up_symk/__init__.py b/up_symk/__init__.py index f6f6a0a..9d3055e 100644 --- a/up_symk/__init__.py +++ b/up_symk/__init__.py @@ -1 +1,3 @@ +from .symk_base import SymKMixin +from .osp_pddl_writer import OspPDDLWriter from .symk import SymKOptimalPDDLPlanner, SymKPDDLPlanner \ No newline at end of file diff --git a/up_symk/osp_patch_file.patch b/up_symk/osp_patch_file.patch new file mode 100644 index 0000000..95e4386 --- /dev/null +++ b/up_symk/osp_patch_file.patch @@ -0,0 +1,2850 @@ +diff --git a/src/h2-preprocessor/helper_functions.cc b/src/h2-preprocessor/helper_functions.cc +index 11024d522..69dd68999 100644 +--- a/src/h2-preprocessor/helper_functions.cc ++++ b/src/h2-preprocessor/helper_functions.cc +@@ -109,6 +109,48 @@ void read_axioms(istream &in, const vector &variables, + axioms.push_back(Axiom(in, variables)); + } + ++void read_utils(istream &in, const vector &variables, vector> &utils) { ++ string word; ++ in >> word; ++ if (word != "begin_util") { ++ // set pointer back ++ in.seekg(-word.length(), std::ios::cur); ++ return; ++ } ++ int count; ++ in >> count; ++ for (int i = 0; i < count; i++) { ++ int varNo, val, util; ++ in >> varNo >> val >> util; ++ utils.push_back(make_tuple(variables[varNo], val, util)); ++ } ++ check_magic(in, "end_util"); ++} ++ ++void read_constant_util(istream &in, int &constant_util) { ++ string word; ++ in >> word; ++ if (word != "begin_constant_util") { ++ // set pointer back ++ in.seekg(-word.length(), std::ios::cur); ++ return; ++ } ++ in >> constant_util; ++ check_magic(in, "end_constant_util"); ++} ++ ++void read_bound(istream &in, int &bound) { ++ string word; ++ in >> word; ++ if (word != "begin_bound") { ++ // set pointer back ++ in.seekg(-word.length(), std::ios::cur); ++ return; ++ } ++ in >> bound; ++ check_magic(in, "end_bound"); ++} ++ + void read_preprocessed_problem_description(istream &in, + bool &metric, + vector &internal_variables, +@@ -117,13 +159,22 @@ void read_preprocessed_problem_description(istream &in, + State &initial_state, + vector> &goals, + vector &operators, +- vector &axioms) { ++ vector &axioms, ++ vector> &utils, ++ int &constant_util, ++ int &bound) { + read_and_verify_version(in); + read_metric(in, metric); + read_variables(in, internal_variables, variables); + read_mutexes(in, mutexes, variables); + initial_state = State(in, variables); + read_goal(in, variables, goals); ++ ++ // try to read osp things ++ read_utils(in, variables, utils); ++ read_constant_util(in, constant_util); ++ read_bound(in, bound); ++ + read_operators(in, variables, operators); + read_axioms(in, variables, axioms); + } +@@ -202,6 +253,71 @@ void generate_cpp_input(const vector &ordered_vars, + + outfile.close(); + } ++ ++void generate_cpp_osp_input(const vector &vars, ++ const bool &metric, ++ const vector &mutexes, ++ const State &initial_state, ++ const vector &operators, ++ const vector &axioms, ++ vector> &utils, ++ int constant_util, ++ int bound ++ ) { ++ ofstream outfile; ++ outfile.open("output.sas", ios::out); ++ ++ outfile << "begin_version" << endl; ++ outfile << PRE_FILE_VERSION << endl; ++ outfile << "end_version" << endl; ++ ++ outfile << "begin_metric" << endl; ++ outfile << metric << endl; ++ outfile << "end_metric" << endl; ++ ++ outfile << vars.size() << endl; ++ for (Variable *var : vars) ++ var->generate_cpp_input(outfile); ++ ++ outfile << mutexes.size() << endl; ++ for (const MutexGroup &mutex : mutexes) ++ mutex.generate_cpp_input(outfile); ++ ++ outfile << "begin_state" << endl; ++ for (Variable *var : vars) ++ outfile << initial_state[var] << endl; // for axioms default value ++ outfile << "end_state" << endl; ++ ++ outfile << "begin_goal" << endl; ++ outfile << "0" << endl; ++ outfile << "end_goal" << endl; ++ ++ outfile << "begin_util" << endl; ++ outfile << utils.size() << endl; ++ for (const auto &util : utils) { ++ outfile << get<0>(util)->get_level() << " " << get<1>(util) << " " << get<2>(util) << endl; ++ } ++ outfile << "end_util" << endl; ++ ++ outfile << "begin_constant_util" << endl; ++ outfile << constant_util << endl; ++ outfile << "end_constant_util" << endl; ++ ++ outfile << "begin_bound" << endl; ++ outfile << bound << endl; ++ outfile << "end_bound" << endl; ++ ++ outfile << operators.size() << endl; ++ for (const Operator &op : operators) ++ op.generate_cpp_input(outfile); ++ ++ outfile << axioms.size() << endl; ++ for (const Axiom &axiom : axioms) ++ axiom.generate_cpp_input(outfile); ++ ++ outfile.close(); ++} ++ + void generate_unsolvable_cpp_input() { + ofstream outfile; + outfile.open("output.sas", ios::out); +diff --git a/src/h2-preprocessor/helper_functions.h b/src/h2-preprocessor/helper_functions.h +index cfaf2488b..25fa10e12 100644 +--- a/src/h2-preprocessor/helper_functions.h ++++ b/src/h2-preprocessor/helper_functions.h +@@ -16,15 +16,19 @@ class Operator; + class Axiom; + + //void read_everything +-void read_preprocessed_problem_description(istream & in, ++void read_preprocessed_problem_description(istream &in, + bool &metric, + vector &internal_variables, + vector &variables, + vector &mutexes, +- State & initial_state, ++ State &initial_state, + vector> &goals, + vector &operators, +- vector &axioms); ++ vector &axioms, ++ vector> &utils, ++ int &constant_util, ++ int &bound); ++ + + //void dump_everything + void dump_preprocessed_problem_description(const vector &variables, +@@ -34,6 +38,7 @@ void dump_preprocessed_problem_description(const vector &variables, + const vector &axioms); + + void generate_unsolvable_cpp_input(); ++ + void generate_cpp_input(const vector &ordered_var, + const bool &metric, + const vector &mutexes, +@@ -41,6 +46,17 @@ void generate_cpp_input(const vector &ordered_var, + const vector> &goals, + const vector &operators, + const vector &axioms); +-void check_magic(istream & in, string magic); ++ ++void generate_cpp_osp_input(const vector &vars, ++ const bool &metric, ++ const vector &mutexes, ++ const State &initial_state, ++ const vector &operators, ++ const vector &axioms, ++ vector> &utils, ++ int constant_util, ++ int bound); ++ ++void check_magic(istream &in, string magic); + + #endif +diff --git a/src/h2-preprocessor/planner.cc b/src/h2-preprocessor/planner.cc +index 0e40ce3e0..c1d1935e3 100644 +--- a/src/h2-preprocessor/planner.cc ++++ b/src/h2-preprocessor/planner.cc +@@ -13,6 +13,7 @@ + #include "axiom.h" + #include "h2_mutexes.h" + #include "variable.h" ++#include + #include + using namespace std; + +@@ -31,6 +32,11 @@ int main(int argc, const char **argv) { + vector operators; + vector axioms; + ++ // osp stuff which may replace goals ++ vector> utils; ++ int constant_util = -1; ++ int bound = -1; ++ + for (int i = 1; i < argc; ++i) { + string arg = string(argv[i]); + if (arg.compare("--no_rel") == 0) { +@@ -65,10 +71,24 @@ int main(int argc, const char **argv) { + } + + read_preprocessed_problem_description +- (cin, metric, internal_variables, variables, mutexes, initial_state, goals, operators, axioms); ++ (cin, metric, internal_variables, variables, mutexes, initial_state, goals, operators, axioms, utils, constant_util, bound); + //dump_preprocessed_problem_description + // (variables, initial_state, goals, operators, axioms); + ++ assert(goals.size() == 0 || utils.size() == 0); ++ ++ if (utils.size() > 0) { ++ assert(goals.size() == 0); ++ cout << "Disabling preprocessing because it does not currently support utilities." << endl; ++ for (size_t i = 0; i < variables.size(); ++i) { ++ variables.at(i)->set_level(i); ++ } ++ generate_cpp_osp_input( ++ variables, metric, mutexes, initial_state, operators, axioms, utils, constant_util, bound); ++ cout << "done" << endl; ++ return 0; ++ } ++ + cout << "Building causal graph..." << endl; + CausalGraph causal_graph(variables, operators, axioms, goals); + const vector &ordering = causal_graph.get_variable_ordering(); +@@ -81,7 +101,7 @@ int main(int argc, const char **argv) { + + // compute h2 mutexes + if (axioms.size() > 0) { +- cout << "Disabling h2 analysis because it does not currently support axioms" << endl; ++ cout << "Disabling h2 analysis because it does not currently support axioms." << endl; + } else if (h2_mutex_time) { + bool conditional_effects = false; + for (const Operator &op : operators) { +@@ -224,7 +244,7 @@ int main(int argc, const char **argv) { + } + } + // Calculate the problem size +- int task_size = ordering.size() + facts + goals.size(); ++ int task_size = ordering.size() + facts + goals.size() + utils.size(); + + for (const MutexGroup &mutex : mutexes) + task_size += mutex.get_encoding_size(); +diff --git a/src/search/DownwardFiles.cmake b/src/search/DownwardFiles.cmake +index eec2a7353..77b9b174c 100644 +--- a/src/search/DownwardFiles.cmake ++++ b/src/search/DownwardFiles.cmake +@@ -806,40 +806,43 @@ fast_downward_plugin( + NAME SYMBOLIC + HELP "Plugin containing the base for symbolic search" + SOURCES +- symbolic/sym_bucket +- symbolic/opt_order +- symbolic/sym_variables +- symbolic/sym_enums +- symbolic/sym_utils +- symbolic/sym_state_space_manager +- symbolic/transition_relation +- symbolic/original_state_space +- symbolic/sym_params_search +- symbolic/sym_estimate ++ symbolic/closed_list + symbolic/frontier + symbolic/open_list +- symbolic/closed_list +- symbolic/searches/bidirectional_search +- symbolic/searches/uniform_cost_search +- symbolic/searches/sym_search +- symbolic/searches/top_k_uniform_cost_search +- symbolic/search_engines/symbolic_search +- symbolic/search_engines/symbolic_uniform_cost_search +- symbolic/search_engines/top_k_symbolic_uniform_cost_search +- symbolic/search_engines/top_q_symbolic_uniform_cost_search ++ symbolic/opt_order ++ symbolic/original_state_space ++ symbolic/plan_reconstruction/reconstruction_node + symbolic/plan_reconstruction/sym_solution_cut + symbolic/plan_reconstruction/sym_solution_registry +- symbolic/plan_reconstruction/reconstruction_node + symbolic/plan_selection/iterative_cost_selector + symbolic/plan_selection/plan_selector +- symbolic/plan_selection/top_k_selector +- symbolic/plan_selection/top_k_even_selector + symbolic/plan_selection/simple_selector ++ symbolic/plan_selection/top_k_even_selector ++ symbolic/plan_selection/top_k_selector + symbolic/plan_selection/unordered_selector + symbolic/plan_selection/validation_selector ++ symbolic/search_engines/symbolic_osp_search ++ symbolic/search_engines/symbolic_osp_top_k_search ++ symbolic/search_engines/symbolic_search ++ symbolic/search_engines/symbolic_uniform_cost_search ++ symbolic/search_engines/top_k_symbolic_uniform_cost_search ++ symbolic/search_engines/top_q_symbolic_uniform_cost_search ++ symbolic/searches/bidirectional_search ++ symbolic/searches/osp_cost_search ++ symbolic/searches/sym_search ++ symbolic/searches/top_k_uniform_cost_search ++ symbolic/searches/uniform_cost_search + symbolic/sym_axiom/sym_axiom_compilation ++ symbolic/sym_bucket ++ symbolic/sym_enums ++ symbolic/sym_estimate + symbolic/sym_function_creator +- DEPENDS SDAC ++ symbolic/sym_params_search ++ symbolic/sym_state_space_manager ++ symbolic/sym_utils ++ symbolic/sym_variables ++ symbolic/transition_relation ++ DEPENDS SDAC + ) + + fast_downward_add_plugin_sources(PLANNER_SOURCES) +diff --git a/src/search/abstract_task.h b/src/search/abstract_task.h +index 291a11ddd..b004ce1b8 100644 +--- a/src/search/abstract_task.h ++++ b/src/search/abstract_task.h +@@ -7,6 +7,7 @@ + #include "utils/hash.h" + #include "mutex_group.h" + ++#include + #include + #include + #include +@@ -96,6 +97,12 @@ public: + virtual std::vector get_initial_state_values() const = 0; + + virtual std::vector get_mutex_groups() const = 0; ++ ++ virtual int get_num_utilties() const = 0; ++ virtual std::pair get_utility(int index) const = 0; ++ virtual int get_constant_utility() const = 0; ++ virtual int get_plan_cost_bound() const = 0; ++ + /* + Convert state values from an ancestor task A (ancestor) into + state values from this task, C (child). Task A has to be an +diff --git a/src/search/ext/cudd-3.0.0/cplusplus/cuddObj.cc b/src/search/ext/cudd-3.0.0/cplusplus/cuddObj.cc +index aa4c37047..1b4550e75 100644 +--- a/src/search/ext/cudd-3.0.0/cplusplus/cuddObj.cc ++++ b/src/search/ext/cudd-3.0.0/cplusplus/cuddObj.cc +@@ -616,7 +616,7 @@ ostream &operator<<(ostream &os, BDD const &f) { + DdManager *mgr = f.p->manager; + vector const &vn = f.p->varnames; + char const *const *inames = vn.size() == (size_t)Cudd_ReadSize(mgr) ? +- &vn[0] : 0; ++ &vn[0] : 0; + char *str = Cudd_FactoredFormString(mgr, f.node, inames); + f.checkReturnValue(str); + os << string(str); +@@ -2380,62 +2380,62 @@ ADD::Xnor( + return ADD(p, result); + } // ADD::Xnor + +-ADD ADD::Equals(const ADD& g) const { ++ADD ADD::Equals(const ADD &g) const { + DdManager *mgr = checkSameManager(g); + DdNode *result = Cudd_addApply(mgr, Cudd_addEquals, node, g.node); + checkReturnValue(result); + return ADD(p, result); + } + +-ADD ADD::NotEquals(const ADD& g) const { ++ADD ADD::NotEquals(const ADD &g) const { + DdManager *mgr = checkSameManager(g); + DdNode *result = Cudd_addApply(mgr, Cudd_addNotEquals, node, g.node); + checkReturnValue(result); + return ADD(p, result); + } + +-ADD ADD::GreaterThan(const ADD& g) const { ++ADD ADD::GreaterThan(const ADD &g) const { + DdManager *mgr = checkSameManager(g); + DdNode *result = Cudd_addApply(mgr, Cudd_addGreaterThan, node, g.node); + checkReturnValue(result); + return ADD(p, result); + } + +-ADD ADD::GreaterThanEquals(const ADD& g) const { ++ADD ADD::GreaterThanEquals(const ADD &g) const { + DdManager *mgr = checkSameManager(g); + DdNode *result = Cudd_addApply(mgr, Cudd_addGreaterThanEquals, node, g.node); + checkReturnValue(result); + return ADD(p, result); + } + +-ADD ADD::LessThan(const ADD& g) const { ++ADD ADD::LessThan(const ADD &g) const { + DdManager *mgr = checkSameManager(g); + DdNode *result = Cudd_addApply(mgr, Cudd_addLessThan, node, g.node); + checkReturnValue(result); + return ADD(p, result); + } + +-ADD ADD::LessThanEquals(const ADD& g) const { ++ADD ADD::LessThanEquals(const ADD &g) const { + DdManager *mgr = checkSameManager(g); + DdNode *result = Cudd_addApply(mgr, Cudd_addLessThanEquals, node, g.node); + checkReturnValue(result); + return ADD(p, result); + } + +-ADD ADD::Pow(const ADD& g) const { ++ADD ADD::Pow(const ADD &g) const { + DdManager *mgr = checkSameManager(g); + DdNode *result = Cudd_addApply(mgr, Cudd_addPow, node, g.node); + checkReturnValue(result); + return ADD(p, result); + } +-ADD ADD::Mod(const ADD& g) const { ++ADD ADD::Mod(const ADD &g) const { + DdManager *mgr = checkSameManager(g); + DdNode *result = Cudd_addApply(mgr, Cudd_addMod, node, g.node); + checkReturnValue(result); + return ADD(p, result); + } + +-ADD ADD::LogXY(const ADD& g) const { ++ADD ADD::LogXY(const ADD &g) const { + DdManager *mgr = checkSameManager(g); + DdNode *result = Cudd_addApply(mgr, Cudd_addLogXY, node, g.node); + checkReturnValue(result); +diff --git a/src/search/ext/cudd-3.0.0/cudd/cudd.h b/src/search/ext/cudd-3.0.0/cudd/cudd.h +index 6c6198ef7..0cbde7e90 100644 +--- a/src/search/ext/cudd-3.0.0/cudd/cudd.h ++++ b/src/search/ext/cudd-3.0.0/cudd/cudd.h +@@ -663,7 +663,7 @@ extern int Cudd_bddVarIsBound(DdManager *dd, int index); + extern DdNode *Cudd_addExistAbstract(DdManager *manager, DdNode *f, DdNode *cube); + extern DdNode *Cudd_addUnivAbstract(DdManager *manager, DdNode *f, DdNode *cube); + extern DdNode *Cudd_addOrAbstract(DdManager *manager, DdNode *f, DdNode *cube); +-extern DdNode *Cudd_addMinAbstract (DdManager *manager, DdNode *f, DdNode *cube); ++extern DdNode *Cudd_addMinAbstract(DdManager *manager, DdNode *f, DdNode *cube); + extern DdNode *Cudd_addApply(DdManager *dd, DD_AOP op, DdNode *f, DdNode *g); + extern DdNode *Cudd_addPlus(DdManager *dd, DdNode **f, DdNode **g); + extern DdNode *Cudd_addTimes(DdManager *dd, DdNode **f, DdNode **g); +@@ -682,15 +682,15 @@ extern DdNode *Cudd_addNor(DdManager *dd, DdNode **f, DdNode **g); + extern DdNode *Cudd_addXor(DdManager *dd, DdNode **f, DdNode **g); + extern DdNode *Cudd_addXnor(DdManager *dd, DdNode **f, DdNode **g); + +-extern DdNode *Cudd_addEquals (DdManager *dd, DdNode **f, DdNode **g); +-extern DdNode *Cudd_addNotEquals (DdManager *dd, DdNode **f, DdNode **g); +-extern DdNode *Cudd_addGreaterThan (DdManager *dd, DdNode **f, DdNode **g); +-extern DdNode *Cudd_addGreaterThanEquals (DdManager *dd, DdNode **f, DdNode **g); +-extern DdNode *Cudd_addLessThan (DdManager *dd, DdNode **f, DdNode **g); +-extern DdNode *Cudd_addLessThanEquals (DdManager *dd, DdNode **f, DdNode **g); +-extern DdNode *Cudd_addPow (DdManager *dd, DdNode **f, DdNode **g); +-extern DdNode *Cudd_addMod (DdManager *dd, DdNode **f, DdNode **g); +-extern DdNode *Cudd_addLogXY (DdManager *dd, DdNode **f, DdNode **g); ++extern DdNode *Cudd_addEquals(DdManager *dd, DdNode **f, DdNode **g); ++extern DdNode *Cudd_addNotEquals(DdManager *dd, DdNode **f, DdNode **g); ++extern DdNode *Cudd_addGreaterThan(DdManager *dd, DdNode **f, DdNode **g); ++extern DdNode *Cudd_addGreaterThanEquals(DdManager *dd, DdNode **f, DdNode **g); ++extern DdNode *Cudd_addLessThan(DdManager *dd, DdNode **f, DdNode **g); ++extern DdNode *Cudd_addLessThanEquals(DdManager *dd, DdNode **f, DdNode **g); ++extern DdNode *Cudd_addPow(DdManager *dd, DdNode **f, DdNode **g); ++extern DdNode *Cudd_addMod(DdManager *dd, DdNode **f, DdNode **g); ++extern DdNode *Cudd_addLogXY(DdManager *dd, DdNode **f, DdNode **g); + + extern DdNode *Cudd_addMonadicApply(DdManager *dd, DD_MAOP op, DdNode *f); + extern DdNode *Cudd_addLog(DdManager *dd, DdNode *f); +diff --git a/src/search/ext/cudd-3.0.0/cudd/cuddInt.h b/src/search/ext/cudd-3.0.0/cudd/cuddInt.h +index 1032430da..fbe80059d 100644 +--- a/src/search/ext/cudd-3.0.0/cudd/cuddInt.h ++++ b/src/search/ext/cudd-3.0.0/cudd/cuddInt.h +@@ -1025,7 +1025,7 @@ struct DdLevelQueue { + (dd)->recursiveCalls, (dd)->keys, \ + (dd)->keys - (dd)->dead, \ + (dd)->nodesDropped, (dd)->reclaimed); \ +- (dd)->nextSample += 250000; } \ ++ (dd)->nextSample += 250000;} \ + } \ + while (0) + #else +@@ -1069,8 +1069,8 @@ extern "C" { + extern DdNode *cuddAddExistAbstractRecur(DdManager *manager, DdNode *f, DdNode *cube); + extern DdNode *cuddAddUnivAbstractRecur(DdManager *manager, DdNode *f, DdNode *cube); + extern DdNode *cuddAddOrAbstractRecur(DdManager *manager, DdNode *f, DdNode *cube); +-extern DdNode *cuddAddMinAbstractRecur (DdManager *manager, DdNode *f, DdNode *cube); +-extern DdNode *cuddAddApplyRecur(DdManager * dd, DdNode * (*)(DdManager *, DdNode * *, DdNode * *), DdNode * f, DdNode * g); ++extern DdNode *cuddAddMinAbstractRecur(DdManager *manager, DdNode *f, DdNode *cube); ++extern DdNode *cuddAddApplyRecur(DdManager * dd, DdNode * (*)(DdManager *, DdNode **, DdNode **), DdNode *f, DdNode *g); + extern DdNode *cuddAddMonadicApplyRecur(DdManager *dd, DdNode * (*op)(DdManager *, DdNode *), DdNode *f); + extern DdNode *cuddAddScalarInverseRecur(DdManager *dd, DdNode *f, DdNode *epsilon); + extern DdNode *cuddAddIteRecur(DdManager *dd, DdNode *f, DdNode *g, DdNode *h); +@@ -1093,14 +1093,14 @@ extern DdNode *cuddBddTransfer(DdManager *ddS, DdManager *ddD, DdNode *f); + extern DdNode *cuddAddBddDoPattern(DdManager *dd, DdNode *f); + extern int cuddInitCache(DdManager *unique, unsigned int cacheSize, unsigned int maxCacheSize); + extern void cuddCacheInsert(DdManager *table, ptruint op, DdNode *f, DdNode *g, DdNode *h, DdNode *data); +-extern void cuddCacheInsert2(DdManager * table, DdNode * (*)(DdManager *, DdNode *, DdNode *), DdNode * f, DdNode * g, DdNode * data); +-extern void cuddCacheInsert1(DdManager * table, DdNode * (*)(DdManager *, DdNode *), DdNode * f, DdNode * data); ++extern void cuddCacheInsert2(DdManager * table, DdNode * (*)(DdManager *, DdNode *, DdNode *), DdNode *f, DdNode *g, DdNode *data); ++extern void cuddCacheInsert1(DdManager * table, DdNode * (*)(DdManager *, DdNode *), DdNode *f, DdNode *data); + extern DdNode *cuddCacheLookup(DdManager *table, ptruint op, DdNode *f, DdNode *g, DdNode *h); + extern DdNode *cuddCacheLookupZdd(DdManager *table, ptruint op, DdNode *f, DdNode *g, DdNode *h); +-extern DdNode *cuddCacheLookup2(DdManager * table, DdNode * (*)(DdManager *, DdNode *, DdNode *), DdNode * f, DdNode * g); +-extern DdNode *cuddCacheLookup1(DdManager * table, DdNode * (*)(DdManager *, DdNode *), DdNode * f); +-extern DdNode *cuddCacheLookup2Zdd(DdManager * table, DdNode * (*)(DdManager *, DdNode *, DdNode *), DdNode * f, DdNode * g); +-extern DdNode *cuddCacheLookup1Zdd(DdManager * table, DdNode * (*)(DdManager *, DdNode *), DdNode * f); ++extern DdNode *cuddCacheLookup2(DdManager * table, DdNode * (*)(DdManager *, DdNode *, DdNode *), DdNode *f, DdNode *g); ++extern DdNode *cuddCacheLookup1(DdManager * table, DdNode * (*)(DdManager *, DdNode *), DdNode *f); ++extern DdNode *cuddCacheLookup2Zdd(DdManager * table, DdNode * (*)(DdManager *, DdNode *, DdNode *), DdNode *f, DdNode *g); ++extern DdNode *cuddCacheLookup1Zdd(DdManager * table, DdNode * (*)(DdManager *, DdNode *), DdNode *f); + extern DdNode *cuddConstantLookup(DdManager *table, ptruint op, DdNode *f, DdNode *g, DdNode *h); + extern int cuddCacheProfile(DdManager *table, FILE *fp); + extern void cuddCacheResize(DdManager *table); +diff --git a/src/search/plan_manager.cc b/src/search/plan_manager.cc +index efbedbd6e..21253fc36 100644 +--- a/src/search/plan_manager.cc ++++ b/src/search/plan_manager.cc +@@ -44,7 +44,7 @@ void PlanManager::dump_plan(const Plan &plan, + OperatorsProxy operators = task_proxy.get_operators(); + for (OperatorID op_id : plan) { + cout << operators[op_id].get_name() << " (" << operators[op_id].get_cost() +- << ")" << endl; ++ << ")" << endl; + } + int plan_cost = calculate_plan_cost(plan, task_proxy); + utils::g_log << "Plan length: " << plan.size() << " step(s)." << endl; +@@ -71,7 +71,7 @@ void PlanManager::save_plan(const Plan &plan, const TaskProxy &task_proxy, + for (OperatorID op_id : plan) { + if (dump_plan) { + cout << operators[op_id].get_name() << " (" << operators[op_id].get_cost() +- << ")" << endl; ++ << ")" << endl; + } + outfile << "(" << operators[op_id].get_name() << ")" << endl; + } +diff --git a/src/search/search_engine.cc b/src/search/search_engine.cc +index 919345d76..8354071ef 100644 +--- a/src/search/search_engine.cc ++++ b/src/search/search_engine.cc +@@ -52,6 +52,7 @@ SearchEngine::SearchEngine(const Options &opts) + search_space(state_registry, log), + statistics(log), + cost_type(opts.get("cost_type")), ++ is_oversubscribed(task_properties::is_oversubscribed(task_proxy)), + is_unit_cost(task_properties::is_unit_cost(task_proxy)), + has_sdac_cost(task_properties::has_sdac_cost_operator(task_proxy)), + max_time(opts.get("max_time")) { +diff --git a/src/search/search_engine.h b/src/search/search_engine.h +index 8ea065309..c1e9e8e0d 100644 +--- a/src/search/search_engine.h ++++ b/src/search/search_engine.h +@@ -50,6 +50,7 @@ protected: + SearchStatistics statistics; + int bound; + OperatorCost cost_type; ++ bool is_oversubscribed; + bool is_unit_cost; + bool has_sdac_cost; + double max_time; +diff --git a/src/search/search_engines/eager_search.cc b/src/search/search_engines/eager_search.cc +index 421b6425a..a90ac2ca2 100644 +--- a/src/search/search_engines/eager_search.cc ++++ b/src/search/search_engines/eager_search.cc +@@ -33,6 +33,11 @@ EagerSearch::EagerSearch(const Options &opts) + cerr << "lazy_evaluator must cache its estimates" << endl; + utils::exit_with(utils::ExitCode::SEARCH_INPUT_ERROR); + } ++ if (is_oversubscribed) { ++ cerr << "error: explicit search does not support oversubscribed tasks. " ++ << "Please use symbolic search." << endl; ++ utils::exit_with(utils::ExitCode::SEARCH_INPUT_ERROR); ++ } + if (has_sdac_cost) { + cerr << "error: explicit search does not support state-dependent action costs. " + << "Please use symbolic search." << endl; +diff --git a/src/search/search_engines/enforced_hill_climbing_search.cc b/src/search/search_engines/enforced_hill_climbing_search.cc +index 189fb8f63..976d90997 100644 +--- a/src/search/search_engines/enforced_hill_climbing_search.cc ++++ b/src/search/search_engines/enforced_hill_climbing_search.cc +@@ -77,6 +77,11 @@ EnforcedHillClimbingSearch::EnforcedHillClimbingSearch( + current_phase_start_g(-1), + num_ehc_phases(0), + last_num_expanded(-1) { ++ if (is_oversubscribed) { ++ cerr << "error: explicit search does not support oversubscribed tasks. " ++ << "Please use symbolic search." << endl; ++ utils::exit_with(utils::ExitCode::SEARCH_INPUT_ERROR); ++ } + if (has_sdac_cost) { + cerr << "error: explicit search does not support state-dependent action costs. " + << "Please use symbolic search." << endl; +diff --git a/src/search/search_engines/lazy_search.cc b/src/search/search_engines/lazy_search.cc +index 0385a069a..b0561fbc8 100644 +--- a/src/search/search_engines/lazy_search.cc ++++ b/src/search/search_engines/lazy_search.cc +@@ -35,6 +35,11 @@ LazySearch::LazySearch(const Options &opts) + We initialize current_eval_context in such a way that the initial node + counts as "preferred". + */ ++ if (is_oversubscribed) { ++ cerr << "error: explicit search does not support oversubscribed tasks. " ++ << "Please use symbolic search." << endl; ++ utils::exit_with(utils::ExitCode::SEARCH_INPUT_ERROR); ++ } + if (has_sdac_cost) { + cerr << "error: explicit search does not support state-dependent action costs. " + << "Please use symbolic search." << endl; +diff --git a/src/search/symbolic/plan_reconstruction/sym_solution_cut.cc b/src/search/symbolic/plan_reconstruction/sym_solution_cut.cc +index ab9e1b65b..f79c1cd40 100644 +--- a/src/search/symbolic/plan_reconstruction/sym_solution_cut.cc ++++ b/src/search/symbolic/plan_reconstruction/sym_solution_cut.cc +@@ -8,21 +8,41 @@ using namespace std; + namespace symbolic { + SymSolutionCut::SymSolutionCut() : + g(-1), +- h(-1) {} ++ h(-1), ++ util(-numeric_limits::max()) {} + +-SymSolutionCut::SymSolutionCut(int g, int h, BDD cut) : ++SymSolutionCut::SymSolutionCut(int g, int h, int util, BDD cut) : + g(g), + h(h), ++ util(util), + cut(cut) {} + ++ ++SymSolutionCut::SymSolutionCut(int g, int h, BDD cut) : ++ SymSolutionCut(g, h, -1, cut) {} ++ + int SymSolutionCut::get_g() const {return g;} + + int SymSolutionCut::get_h() const {return h;} + + int SymSolutionCut::get_f() const {return g + h;} + ++int SymSolutionCut::get_util() const {return util;} ++ + BDD SymSolutionCut::get_cut() const {return cut;} + ++int SymSolutionCut::get_priority() const { ++ if (get_util() != -numeric_limits::max()) { ++ // Negative to assign higest prio to max util ++ return -get_util(); ++ } ++ return get_f(); ++} ++ ++bool SymSolutionCut::has_utility() const { ++ return get_util() != -numeric_limits::max(); ++} ++ + void SymSolutionCut::merge(const SymSolutionCut &other) { + assert(*this == other); + cut += other.get_cut(); +@@ -32,25 +52,33 @@ void SymSolutionCut::set_g(int g) {this->g = g;} + + void SymSolutionCut::set_h(int h) {this->h = h;} + ++void SymSolutionCut::set_util(int util) {this->util = util;} ++ + void SymSolutionCut::set_cut(BDD cut) {this->cut = cut;} + + bool SymSolutionCut::operator<(const SymSolutionCut &other) const { +- bool result = get_f() < other.get_f(); +- result |= (get_f() == other.get_f() && get_g() < other.get_g()); +- return result; ++ if (get_util() > other.get_util()) ++ return true; ++ if (get_util() < other.get_util()) ++ return false; ++ if (get_f() < other.get_f()) ++ return true; ++ if (get_f() > other.get_f()) ++ return false; ++ if (get_g() < other.get_g()) ++ return true; ++ return false; + } + + bool SymSolutionCut::operator>(const SymSolutionCut &other) const { +- bool result = get_f() > other.get_f(); +- result |= (get_f() == other.get_f() && get_g() > other.get_g()); +- return result; ++ return !(*this < other); + } + + bool SymSolutionCut::operator==(const SymSolutionCut &other) const { +- return get_g() == other.get_g() && get_h() == other.get_h(); ++ return get_util() == other.get_util() && get_g() == other.get_g() && get_h() == other.get_h(); + } + + bool SymSolutionCut::operator!=(const SymSolutionCut &other) const { +- return !(get_g() == other.get_g() && get_h() == other.get_h()); ++ return !(*this == other); + } + } // namespace symbolic +diff --git a/src/search/symbolic/plan_reconstruction/sym_solution_cut.h b/src/search/symbolic/plan_reconstruction/sym_solution_cut.h +index d57bfbe15..66bd41668 100644 +--- a/src/search/symbolic/plan_reconstruction/sym_solution_cut.h ++++ b/src/search/symbolic/plan_reconstruction/sym_solution_cut.h +@@ -11,21 +11,30 @@ class SymSolutionCut { + protected: + int g; + int h; ++ int util; // utility for osp + BDD cut; + + public: + SymSolutionCut(); // dummy for no solution ++ SymSolutionCut(int g, int h, int util, BDD cut); + SymSolutionCut(int g, int h, BDD cut); + + int get_g() const; + int get_h() const; + int get_f() const; ++ int get_util() const; + BDD get_cut() const; + ++ // Sorting of cuts: usually f but for osp it is util ++ int get_priority() const; ++ ++ bool has_utility() const; ++ + void merge(const SymSolutionCut &other); + + void set_g(int g); + void set_h(int h); ++ void set_util(int util); + void set_cut(BDD cut); + + // Here we only compare g and h values!!! +@@ -38,6 +47,7 @@ public: + const SymSolutionCut &sym_cut) { + return os << "symcut{g=" << sym_cut.get_g() << ", h=" << sym_cut.get_h() + << ", f=" << sym_cut.get_f() ++ << ", u=" << sym_cut.get_util() + << ", nodes=" << sym_cut.get_cut().nodeCount() << "}"; + } + }; +diff --git a/src/search/symbolic/plan_reconstruction/sym_solution_registry.cc b/src/search/symbolic/plan_reconstruction/sym_solution_registry.cc +index 48ebc8d00..73948a3ee 100644 +--- a/src/search/symbolic/plan_reconstruction/sym_solution_registry.cc ++++ b/src/search/symbolic/plan_reconstruction/sym_solution_registry.cc +@@ -230,39 +230,65 @@ void SymSolutionRegistry::register_solution(const SymSolutionCut &solution) { + if (!sym_cuts.empty()) { + sym_cuts = map>(); + } +- sym_cuts[solution.get_f()].push_back(solution); ++ sym_cuts[solution.get_priority()].push_back(solution); + return; + } + ++ // // We skip the merging optimization in case we have utilities ++ // if (solution.has_utility()) { ++ // sym_cuts[solution.get_priority()].push_back(solution); ++ // return; ++ // } ++ + bool merged = false; + size_t pos = 0; +- for (; pos < sym_cuts[solution.get_f()].size(); pos++) { ++ for (; pos < sym_cuts[solution.get_priority()].size(); pos++) { + // a cut with same g and h values exist + // => we combine the cut to avoid multiple cuts with same solutions +- if (sym_cuts[solution.get_f()][pos] == solution) { +- sym_cuts[solution.get_f()][pos].merge(solution); ++ if (sym_cuts[solution.get_priority()][pos] == solution) { ++ sym_cuts[solution.get_priority()][pos].merge(solution); + merged = true; + break; + } + } + if (!merged) { +- sym_cuts[solution.get_f()].push_back(solution); ++ sym_cuts[solution.get_priority()].push_back(solution); + } + } + +-void SymSolutionRegistry::construct_cheaper_solutions(int bound) { ++void SymSolutionRegistry::construct_better_cost_solutions(int upper_cost_bound) { + for (const auto &key : sym_cuts) { + int plan_cost = key.first; + const vector &cuts = key.second; +- if (plan_cost >= bound || found_all_plans()) ++ if (plan_cost >= upper_cost_bound || found_all_plans()) + break; +- + reconstruct_plans(cuts); + } + + // Erase handled keys + for (auto it = sym_cuts.begin(); it != sym_cuts.end();) { +- (it->first < bound) ? sym_cuts.erase(it++) : (++it); ++ (it->first < upper_cost_bound) ? sym_cuts.erase(it++) : (++it); ++ } ++} ++ ++void SymSolutionRegistry::construct_better_utility_solutions(int lower_utility_bound) { ++ for (const auto &key : sym_cuts) { ++ int plan_utility = -key.first; // negation because we use negative prios ++ const vector &cuts = key.second; ++ if (plan_utility <= lower_utility_bound || found_all_plans()) ++ break; ++ reconstruct_plans(cuts); ++ } ++ ++ // Erase handled keys (negation because we use negative prios) ++ for (auto it = sym_cuts.begin(); it != sym_cuts.end();) { ++ (-it->first > lower_utility_bound) ? sym_cuts.erase(it++) : (++it); + } + } ++ ++void SymSolutionRegistry::reconstruct_solution(const SymSolutionCut &sol) { ++ vector cur_sols; ++ cur_sols.push_back(sol); ++ reconstruct_plans(cur_sols); ++} + } +diff --git a/src/search/symbolic/plan_reconstruction/sym_solution_registry.h b/src/search/symbolic/plan_reconstruction/sym_solution_registry.h +index 202a8c3f9..d858f3336 100644 +--- a/src/search/symbolic/plan_reconstruction/sym_solution_registry.h ++++ b/src/search/symbolic/plan_reconstruction/sym_solution_registry.h +@@ -85,7 +85,9 @@ public: + virtual ~SymSolutionRegistry() = default; + + void register_solution(const SymSolutionCut &solution); +- void construct_cheaper_solutions(int bound); ++ void construct_better_cost_solutions(int upper_cost_bound); ++ void construct_better_utility_solutions(int lower_utility_bound); ++ void reconstruct_solution(const SymSolutionCut &sol); + + bool found_all_plans() const { + return plan_data_base && plan_data_base->found_enough_plans(); +@@ -102,15 +104,15 @@ public: + return plan_data_base->get_states_accepted_goal_path(); + } + +- double cheapest_solution_cost_found() const { +- double cheapest = std::numeric_limits::infinity(); ++ double best_solution_found() const { ++ double best_prio = std::numeric_limits::infinity(); + if (plan_data_base) { +- cheapest = std::min(cheapest, plan_data_base->get_first_plan_cost()); ++ best_prio = std::min(best_prio, plan_data_base->get_first_plan_cost()); + } + if (sym_cuts.size() > 0) { +- cheapest = std::min(cheapest, (double)sym_cuts.begin()->first); ++ best_prio = std::min(best_prio, (double)sym_cuts.begin()->first); + } +- return cheapest; ++ return best_prio; + } + }; + } +diff --git a/src/search/symbolic/search_engines/symbolic_osp_search.cc b/src/search/symbolic/search_engines/symbolic_osp_search.cc +new file mode 100644 +index 000000000..6dee0a6ed +--- /dev/null ++++ b/src/search/symbolic/search_engines/symbolic_osp_search.cc +@@ -0,0 +1,168 @@ ++#include "symbolic_osp_search.h" ++ ++#include "../original_state_space.h" ++#include "../plugin.h" ++#include "../searches/osp_cost_search.h" ++#include "../sym_function_creator.h" ++ ++#include "../../option_parser.h" ++ ++ ++using namespace std; ++ ++namespace symbolic { ++void SymbolicOspSearch::initialize() { ++ SymbolicSearch::initialize(); ++ initialize_utlility(); ++ ++ mgr = make_shared(vars.get(), mgrParams, search_task); ++ unique_ptr fw_search = unique_ptr(new OspCostSearch(this, searchParams)); ++ fw_search->init(mgr, true, nullptr); ++ ++ auto individual_trs = fw_search->getStateSpaceShared()->getIndividualTRs(); ++ ++ solution_registry->init(vars, ++ fw_search->getClosedShared(), ++ nullptr, ++ individual_trs, ++ plan_data_base, ++ single_solution, ++ simple); ++ ++ search.reset(fw_search.release()); ++} ++ ++void SymbolicOspSearch::initialize_utlility() { ++ upper_bound = min(search_task->get_plan_cost_bound(), upper_bound); ++ ++ ADD add_utility_function = create_utility_function(); ++ partition_add_to_bdds(vars.get(), add_utility_function, utility_function); ++ max_utility = utility_function.rbegin()->first; ++ assert(max_utility == round(Cudd_V(add_utility_function.FindMax().getNode()))); ++ ++ int min_utility = round(Cudd_V(add_utility_function.FindMin().getNode())); ++ if (min_utility <= -numeric_limits::max()) { ++ cerr << "Utility values exceed integer limits." << endl; ++ utils::exit_with(utils::ExitCode::SEARCH_INPUT_ERROR); ++ } ++ ++ utils::g_log << "Plan cost bound: " << upper_bound - 1 << endl; ++ utils::g_log << "Constant utility: " << search_task->get_constant_utility() << endl; ++ utils::g_log << "Number of utility facts: " << search_task->get_num_utilties() << endl; ++ utils::g_log << "Max utility value: " << max_utility << endl; ++ cout << endl; ++} ++ ++ADD SymbolicOspSearch::create_utility_function() const { ++ ADD res = vars->constant(search_task->get_constant_utility()); ++ for (const UtilityProxy util: task_proxy.get_utilities()) { ++ BDD fact = vars->get_axiom_compiliation()->get_primary_representation(util.get_fact_pair().var, util.get_fact_pair().value); ++ res += fact.Add() * vars->constant(util.get_utility()); ++ } ++ // vars->to_dot(res, "utility.dot"); ++ return res; ++} ++ ++SymSolutionCut SymbolicOspSearch::get_highest_util_solution(const SymSolutionCut &sol) const { ++ double max_util_value = -1; ++ BDD max_util_states = vars->zeroBDD(); ++ for (auto iter = utility_function.rbegin(); iter != utility_function.rend(); ++iter) { ++ max_util_states = iter->second * sol.get_cut(); ++ if (!max_util_states.IsZero()) { ++ max_util_value = iter->first; ++ break; ++ } ++ } ++ return SymSolutionCut(sol.get_g(), sol.get_h(), round(max_util_value), max_util_states); ++} ++ ++SymbolicOspSearch::SymbolicOspSearch( ++ const options::Options &opts) : ++ SymbolicSearch(opts), ++ max_utility(-numeric_limits::max()), ++ highest_seen_utility(-numeric_limits::max()) { ++ if (!is_oversubscribed) { ++ cerr << "error: osp symbolic search does not support ordinary classical planning tasks. " ++ << "Please use ordinary symbolic search, e.g., sym-bd()." << endl; ++ utils::exit_with(utils::ExitCode::SEARCH_INPUT_ERROR); ++ } ++} ++ ++void SymbolicOspSearch::new_solution(const SymSolutionCut &sol) { ++ auto osp_sol = get_highest_util_solution(sol); ++ if (!solution_registry->found_all_plans() && sol.get_f() < upper_bound && highest_seen_utility < osp_sol.get_util()) { ++ solution_registry->register_solution(osp_sol); ++ highest_seen_utility = osp_sol.get_util(); ++ utils::g_log << "Best util: " << highest_seen_utility << endl; ++ } ++} ++ ++SearchStatus SymbolicOspSearch::step() { ++ step_num++; ++ // Handling empty plan ++ if (step_num == 0) { ++ BDD cut = mgr->getInitialState() * mgr->getGoal(); ++ if (!cut.IsZero()) { ++ new_solution(SymSolutionCut(0, 0, cut)); ++ } ++ } ++ ++ SearchStatus cur_status = IN_PROGRESS; ++ ++ // Search finished! ++ if (lower_bound >= upper_bound) { ++ solution_registry->construct_better_utility_solutions(-numeric_limits::max()); ++ solution_found = plan_data_base->get_num_reported_plan() > 0; ++ cur_status = solution_found ? SOLVED : FAILED; ++ } else if (max_utility == highest_seen_utility) { ++ // Highest utility => Search finished! ++ utils::g_log << "State with overall highest utility reached." << endl; ++ solution_registry->construct_better_utility_solutions(-numeric_limits::max()); ++ if (solution_registry->found_all_plans()) { ++ cur_status = SOLVED; ++ } ++ } ++ if (lower_bound_increased && !silent) { ++ utils::g_log << "BOUND: " << lower_bound << " < " << upper_bound << flush; ++ ++ utils::g_log << " [" << solution_registry->get_num_found_plans() << "/" ++ << plan_data_base->get_num_desired_plans() << " plans]" ++ << flush; ++ utils::g_log << ", total time: " << utils::g_timer << endl; ++ } ++ lower_bound_increased = false; ++ ++ if (cur_status == SOLVED) { ++ set_plan(plan_data_base->get_first_accepted_plan()); ++ cout << endl; ++ return cur_status; ++ } ++ if (cur_status == FAILED) { ++ return cur_status; ++ } ++ ++ // Actuall step ++ search->step(); ++ ++ return cur_status; ++} ++} ++ ++static shared_ptr _parse_forward_osp(OptionParser &parser) { ++ parser.document_synopsis("Symbolic Forward Oversubscription Search", ""); ++ symbolic::SymbolicSearch::add_options_to_parser(parser); ++ parser.add_option>( ++ "plan_selection", "plan selection strategy", "top_k(num_plans=1)"); ++ Options opts = parser.parse(); ++ ++ shared_ptr engine = nullptr; ++ if (!parser.dry_run()) { ++ engine = make_shared(opts); ++ utils::g_log << "Symbolic Forward Oversubscription Search" << endl; ++ } ++ ++ return engine; ++} ++ ++static Plugin _plugin_sym_fw_ordinary("sym-osp-fw", ++ _parse_forward_osp); +diff --git a/src/search/symbolic/search_engines/symbolic_osp_search.h b/src/search/symbolic/search_engines/symbolic_osp_search.h +new file mode 100644 +index 000000000..3ece38ca7 +--- /dev/null ++++ b/src/search/symbolic/search_engines/symbolic_osp_search.h +@@ -0,0 +1,32 @@ ++#ifndef SYMBOLIC_SEARCH_ENGINES_SYMBOLIC_OSP_SEARCH_H ++#define SYMBOLIC_SEARCH_ENGINES_SYMBOLIC_OSP_SEARCH_H ++ ++#include "symbolic_search.h" ++ ++#include ++ ++namespace symbolic { ++class SymbolicOspSearch : public SymbolicSearch { ++protected: ++ std::map utility_function; ++ int max_utility; ++ int highest_seen_utility; ++ ++ virtual void initialize() override; ++ virtual void initialize_utlility(); ++ ++ ADD create_utility_function() const; ++ ++ SymSolutionCut get_highest_util_solution(const SymSolutionCut &sol) const; ++ ++ virtual SearchStatus step() override; ++ ++public: ++ SymbolicOspSearch(const options::Options &opts); ++ virtual ~SymbolicOspSearch() = default; ++ ++ virtual void new_solution(const SymSolutionCut &sol) override; ++}; ++} ++ ++#endif +diff --git a/src/search/symbolic/search_engines/symbolic_osp_top_k_search.cc b/src/search/symbolic/search_engines/symbolic_osp_top_k_search.cc +new file mode 100644 +index 000000000..f082c6ebf +--- /dev/null ++++ b/src/search/symbolic/search_engines/symbolic_osp_top_k_search.cc +@@ -0,0 +1,164 @@ ++#include "symbolic_osp_top_k_search.h" ++ ++#include "../original_state_space.h" ++#include "../plugin.h" ++#include "../searches/osp_cost_search.h" ++#include "../sym_function_creator.h" ++ ++#include "../../option_parser.h" ++ ++ ++using namespace std; ++ ++namespace symbolic { ++void SymbolicOspTopkSearch::initialize() { ++ SymbolicSearch::initialize(); ++ initialize_utlility(); ++ ++ mgr = make_shared(vars.get(), mgrParams, search_task); ++ unique_ptr fw_search = unique_ptr(new OspCostSearch(this, searchParams)); ++ fw_search->init(mgr, true, nullptr); ++ ++ auto individual_trs = fw_search->getStateSpaceShared()->getIndividualTRs(); ++ ++ solution_registry->init(vars, ++ fw_search->getClosedShared(), ++ nullptr, ++ individual_trs, ++ plan_data_base, ++ false, ++ simple); ++ ++ search.reset(fw_search.release()); ++} ++ ++SymbolicOspTopkSearch::SymbolicOspTopkSearch( ++ const options::Options &opts) : ++ SymbolicOspSearch(opts), ++ quality_multiplier(opts.get("quality")) { ++ if (quality_multiplier != numeric_limits::infinity()) ++ utils::g_log << "Quality multiplier: " << quality_multiplier << endl; ++} ++ ++vector SymbolicOspTopkSearch::get_all_util_solutions(const SymSolutionCut &sol) { ++ vector result; ++ for (auto iter = utility_function.rbegin(); iter != utility_function.rend(); ++iter) { ++ BDD util_states = iter->second * sol.get_cut(); ++ if (!util_states.IsZero()) { ++ double util_value = iter->first; ++ result.emplace_back(sol.get_g(), sol.get_h(), round(util_value), util_states); ++ } ++ } ++ return result; ++} ++ ++void SymbolicOspTopkSearch::new_solution(const SymSolutionCut &sol) { ++ if (solution_registry->found_all_plans() || sol.get_f() >= upper_bound) ++ return; ++ ++ for (auto const &osp_sol : get_all_util_solutions(sol)) { ++ // We found a solution with highest possible utility and reconstruct it directly ++ // This is kind of a hack... ++ if (max_utility == osp_sol.get_util()) { ++ utils::g_log << "States with overall highest utility reached." << endl; ++ solution_registry->reconstruct_solution(osp_sol); ++ highest_seen_utility = osp_sol.get_util(); ++ utils::g_log << "Best util: " << highest_seen_utility << endl; ++ } else { ++ solution_registry->register_solution(osp_sol); ++ if (osp_sol.get_util() > highest_seen_utility) { ++ highest_seen_utility = osp_sol.get_util(); ++ utils::g_log << "Best util: " << highest_seen_utility << endl; ++ } ++ } ++ } ++} ++ ++SearchStatus SymbolicOspTopkSearch::step() { ++ step_num++; ++ // Handling empty plan ++ if (step_num == 0) { ++ BDD cut = mgr->getInitialState() * mgr->getGoal(); ++ if (!cut.IsZero()) { ++ new_solution(SymSolutionCut(0, 0, cut)); ++ } ++ } ++ ++ SearchStatus cur_status = IN_PROGRESS; ++ ++ // Search finished! ++ if (lower_bound >= upper_bound) { ++ int lower_utility_bound = get_quality_bound(); ++ if (lower_utility_bound != -numeric_limits::max()) ++ --lower_utility_bound; ++ solution_registry->construct_better_utility_solutions(lower_utility_bound); ++ solution_found = plan_data_base->get_num_reported_plan() > 0; ++ cur_status = solution_found ? SOLVED : FAILED; ++ } ++ ++ if (lower_bound_increased && !silent) { ++ utils::g_log << "BOUND: " << lower_bound << " < " << upper_bound << flush; ++ ++ utils::g_log << " [" << solution_registry->get_num_found_plans() << "/" ++ << plan_data_base->get_num_desired_plans() << " plans]" ++ << flush; ++ utils::g_log << ", total time: " << utils::g_timer << endl; ++ } ++ lower_bound_increased = false; ++ ++ if (cur_status == SOLVED) { ++ set_plan(plan_data_base->get_first_accepted_plan()); ++ cout << endl; ++ return cur_status; ++ } ++ if (cur_status == FAILED) { ++ return cur_status; ++ } ++ ++ // Actuall step ++ search->step(); ++ ++ return cur_status; ++} ++} ++ ++static shared_ptr _parse_forward_top_k_osp(OptionParser &parser) { ++ parser.document_synopsis("Symbolic Forward Oversubscription Top-k Search", ""); ++ symbolic::SymbolicSearch::add_options_to_parser(parser); ++ parser.add_option>( ++ "plan_selection", "plan selection strategy"); ++ Options opts = parser.parse(); ++ opts.set("quality", numeric_limits::infinity()); ++ ++ shared_ptr engine = nullptr; ++ if (!parser.dry_run()) { ++ engine = make_shared(opts); ++ utils::g_log << "Symbolic Forward Oversubscription Top-k Search" << endl; ++ } ++ ++ return engine; ++} ++ ++static shared_ptr _parse_forward_top_q_osp(OptionParser &parser) { ++ parser.document_synopsis("Symbolic Forward Oversubscription Top-q Search", ""); ++ symbolic::SymbolicSearch::add_options_to_parser(parser); ++ parser.add_option>( ++ "plan_selection", "plan selection strategy"); ++ parser.add_option("quality", "relative quality multiplier", ++ "infinity", Bounds("1.0", "infinity")); ++ Options opts = parser.parse(); ++ ++ shared_ptr engine = nullptr; ++ if (!parser.dry_run()) { ++ engine = make_shared(opts); ++ utils::g_log << "Symbolic Forward Oversubscription Top-q Search" << endl; ++ } ++ ++ return engine; ++} ++ ++static Plugin _plugin_sym_fw_top_k_osp("symk-osp-fw", ++ _parse_forward_top_k_osp); ++ ++static Plugin _plugin_sym_fw_top_q_osp("symq-osp-fw", ++ _parse_forward_top_q_osp); +diff --git a/src/search/symbolic/search_engines/symbolic_osp_top_k_search.h b/src/search/symbolic/search_engines/symbolic_osp_top_k_search.h +new file mode 100644 +index 000000000..0c5aa42c6 +--- /dev/null ++++ b/src/search/symbolic/search_engines/symbolic_osp_top_k_search.h +@@ -0,0 +1,38 @@ ++#ifndef SYMBOLIC_SEARCH_ENGINES_SYMBOLIC_OSP_TOP_K_SEARCH_H ++#define SYMBOLIC_SEARCH_ENGINES_SYMBOLIC_OSP_TOP_K_SEARCH_H ++ ++#include "symbolic_osp_search.h" ++ ++#include ++ ++namespace symbolic { ++class SymbolicOspTopkSearch : public SymbolicOspSearch { ++private: ++ int get_quality_bound() { ++ if (quality_multiplier == std::numeric_limits::infinity()) ++ return -std::numeric_limits::max(); ++ // negative utility ++ if (highest_seen_utility < 0) ++ return highest_seen_utility * quality_multiplier; ++ return ceil(highest_seen_utility / quality_multiplier); ++ } ++ ++protected: ++ virtual void initialize() override; ++ virtual SearchStatus step() override; ++ virtual std::vector get_all_util_solutions(const SymSolutionCut &sol); ++ ++ // Let u be the cost of the utility of the best found plan ++ // We want all plans with utility scaled by quality_multiplier ++ // see get_quality_bound() ++ double quality_multiplier; ++ ++public: ++ SymbolicOspTopkSearch(const options::Options &opts); ++ virtual ~SymbolicOspTopkSearch() = default; ++ ++ virtual void new_solution(const SymSolutionCut &sol) override; ++}; ++} ++ ++#endif +diff --git a/src/search/symbolic/search_engines/symbolic_search.cc b/src/search/symbolic/search_engines/symbolic_search.cc +index f4f6c1efe..10a989b60 100644 +--- a/src/search/symbolic/search_engines/symbolic_search.cc ++++ b/src/search/symbolic/search_engines/symbolic_search.cc +@@ -31,7 +31,7 @@ SymbolicSearch::SymbolicSearch(const options::Options &opts) + step_num(-1), + lower_bound_increased(true), + lower_bound(0), +- upper_bound(numeric_limits::max()), ++ upper_bound(bound), + min_g(0), + plan_data_base(opts.get>("plan_selection")), + solution_registry(make_shared()), +@@ -95,14 +95,14 @@ SearchStatus SymbolicSearch::step() { + + // Search finished! + if (lower_bound >= upper_bound) { +- solution_registry->construct_cheaper_solutions( ++ solution_registry->construct_better_cost_solutions( + numeric_limits::max()); + solution_found = plan_data_base->get_num_reported_plan() > 0; + cur_status = solution_found ? SOLVED : FAILED; + } else { + // Bound increased => construct plans + if (lower_bound_increased) { +- solution_registry->construct_cheaper_solutions(lower_bound); ++ solution_registry->construct_better_cost_solutions(lower_bound); + } + + // All plans found +diff --git a/src/search/symbolic/search_engines/symbolic_uniform_cost_search.cc b/src/search/symbolic/search_engines/symbolic_uniform_cost_search.cc +index ae993d7a2..1238ae938 100644 +--- a/src/search/symbolic/search_engines/symbolic_uniform_cost_search.cc ++++ b/src/search/symbolic/search_engines/symbolic_uniform_cost_search.cc +@@ -54,7 +54,13 @@ void SymbolicUniformCostSearch::initialize() { + + SymbolicUniformCostSearch::SymbolicUniformCostSearch( + const options::Options &opts, bool fw, bool bw) +- : SymbolicSearch(opts), fw(fw), bw(bw) {} ++ : SymbolicSearch(opts), fw(fw), bw(bw) { ++ if (is_oversubscribed) { ++ cerr << "error: ordinary symbolic search does not support oversubscribed tasks. " ++ << "Please use symbolic search for osp, e.g., sym-osp-fw()." << endl; ++ utils::exit_with(utils::ExitCode::SEARCH_INPUT_ERROR); ++ } ++} + + void SymbolicUniformCostSearch::new_solution(const SymSolutionCut &sol) { + if (!solution_registry->found_all_plans() && sol.get_f() < upper_bound) { +diff --git a/src/search/symbolic/search_engines/top_q_symbolic_uniform_cost_search.cc b/src/search/symbolic/search_engines/top_q_symbolic_uniform_cost_search.cc +index 7c00d9b0b..f7d9b0dc7 100644 +--- a/src/search/symbolic/search_engines/top_q_symbolic_uniform_cost_search.cc ++++ b/src/search/symbolic/search_engines/top_q_symbolic_uniform_cost_search.cc +@@ -43,13 +43,13 @@ SearchStatus TopqSymbolicUniformCostSearch::step() { + + // Search finished! + if (lower_bound >= upper_bound) { +- solution_registry->construct_cheaper_solutions(upper_bound); ++ solution_registry->construct_better_cost_solutions(upper_bound); + solution_found = plan_data_base->get_num_reported_plan() > 0; + cur_status = solution_found ? SOLVED : FAILED; + } else { + // Bound increade => construct plans + if (lower_bound_increased) { +- solution_registry->construct_cheaper_solutions(lower_bound); ++ solution_registry->construct_better_cost_solutions(lower_bound); + } + + // All plans found +diff --git a/src/search/symbolic/search_engines/top_q_symbolic_uniform_cost_search.h b/src/search/symbolic/search_engines/top_q_symbolic_uniform_cost_search.h +index fc7efa2b3..4802b3e58 100644 +--- a/src/search/symbolic/search_engines/top_q_symbolic_uniform_cost_search.h ++++ b/src/search/symbolic/search_engines/top_q_symbolic_uniform_cost_search.h +@@ -7,7 +7,7 @@ namespace symbolic { + class TopqSymbolicUniformCostSearch : public TopkSymbolicUniformCostSearch { + private: + double get_quality_bound() { +- return solution_registry->cheapest_solution_cost_found() * ++ return solution_registry->best_solution_found() * + quality_multiplier; + } + +diff --git a/src/search/symbolic/searches/osp_cost_search.cc b/src/search/symbolic/searches/osp_cost_search.cc +new file mode 100644 +index 000000000..78b15df97 +--- /dev/null ++++ b/src/search/symbolic/searches/osp_cost_search.cc +@@ -0,0 +1,10 @@ ++#include "osp_cost_search.h" ++#include "../closed_list.h" ++ ++namespace symbolic { ++void OspCostSearch::filterFrontier() { ++ frontier.filter(closed->getClosed()); ++ mgr->filterMutex(frontier.bucket(), fw, initialization()); ++ removeZero(frontier.bucket()); ++} ++} +diff --git a/src/search/symbolic/searches/osp_cost_search.h b/src/search/symbolic/searches/osp_cost_search.h +new file mode 100644 +index 000000000..d721edd67 +--- /dev/null ++++ b/src/search/symbolic/searches/osp_cost_search.h +@@ -0,0 +1,18 @@ ++#ifndef SYMBOLIC_SEARCHES_OSP_COST_SEARCH_H ++#define SYMBOLIC_SEARCHES_OSP_COST_SEARCH_H ++ ++#include "uniform_cost_search.h" ++ ++namespace symbolic { ++class OspCostSearch : public UniformCostSearch { ++protected: ++ ++ virtual void filterFrontier() override; ++ ++public: ++ OspCostSearch(SymbolicSearch *eng, const SymParamsSearch ¶ms) ++ : UniformCostSearch(eng, params) {} ++}; ++} ++ ++#endif +diff --git a/src/search/symbolic/searches/top_k_uniform_cost_search.cc b/src/search/symbolic/searches/top_k_uniform_cost_search.cc +index b984bb7a0..6ffc49b95 100644 +--- a/src/search/symbolic/searches/top_k_uniform_cost_search.cc ++++ b/src/search/symbolic/searches/top_k_uniform_cost_search.cc +@@ -21,8 +21,8 @@ bool TopkUniformCostSearch::provable_no_more_plans() { + return open_list.empty(); + } + +-void TopkUniformCostSearch::checkFrontierCut(Bucket &bucket, int g) { +- for (BDD &bucketBDD : bucket) { ++void TopkUniformCostSearch::checkFrontierCut(const Bucket &bucket, int g) { ++ for (BDD bucketBDD : bucket) { + auto all_sols = + perfectHeuristic->getAllCuts(bucketBDD, g, fw, engine->getMinG()); + for (auto &sol : all_sols) { +diff --git a/src/search/symbolic/searches/top_k_uniform_cost_search.h b/src/search/symbolic/searches/top_k_uniform_cost_search.h +index 0cea017b1..79896242d 100644 +--- a/src/search/symbolic/searches/top_k_uniform_cost_search.h ++++ b/src/search/symbolic/searches/top_k_uniform_cost_search.h +@@ -8,7 +8,7 @@ class TopkUniformCostSearch : public UniformCostSearch { + protected: + virtual bool provable_no_more_plans() override; + +- virtual void checkFrontierCut(Bucket &bucket, int g) override; ++ virtual void checkFrontierCut(const Bucket &bucket, int g) override; + + virtual void filterFrontier() override; + +diff --git a/src/search/symbolic/searches/uniform_cost_search.cc b/src/search/symbolic/searches/uniform_cost_search.cc +index 329529e02..3ca17c73f 100644 +--- a/src/search/symbolic/searches/uniform_cost_search.cc ++++ b/src/search/symbolic/searches/uniform_cost_search.cc +@@ -58,18 +58,16 @@ bool UniformCostSearch::init(shared_ptr manager, + return true; + } + +-void UniformCostSearch::checkFrontierCut(Bucket &bucket, int g) { ++void UniformCostSearch::checkFrontierCut(const Bucket &bucket, int g) { + if (p.get_non_stop()) { + return; + } + +- for (BDD &bucketBDD : bucket) { ++ for (BDD bucketBDD : bucket) { + auto sol = perfectHeuristic->getCheapestCut(bucketBDD, g, fw); + if (sol.get_f() >= 0) { + engine->new_solution(sol); + } +- // Prune everything closed in opposite direction +- bucketBDD *= perfectHeuristic->notClosed(); + } + } + +@@ -110,7 +108,8 @@ bool UniformCostSearch::prepareBucket() { + // This procedure is delayed in comparision to explicit search + // Idea: no need to "change" BDDs until we actually process them + void UniformCostSearch::filterFrontier() { +- frontier.filter(!closed->notClosed()); ++ frontier.filter(perfectHeuristic->getClosed()); ++ frontier.filter(closed->getClosed()); + mgr->filterMutex(frontier.bucket(), fw, initialization()); + removeZero(frontier.bucket()); + } +diff --git a/src/search/symbolic/searches/uniform_cost_search.h b/src/search/symbolic/searches/uniform_cost_search.h +index de2924b08..5b86939b4 100644 +--- a/src/search/symbolic/searches/uniform_cost_search.h ++++ b/src/search/symbolic/searches/uniform_cost_search.h +@@ -61,7 +61,7 @@ protected: + /* + * Check generated or closed states with other frontiers => solution check + */ +- virtual void checkFrontierCut(Bucket &bucket, int g); ++ virtual void checkFrontierCut(const Bucket &bucket, int g); + + void closeStates(Bucket &bucket, int g); + +diff --git a/src/search/symbolic/sym_state_space_manager.h b/src/search/symbolic/sym_state_space_manager.h +index 682416118..3b5da4251 100644 +--- a/src/search/symbolic/sym_state_space_manager.h ++++ b/src/search/symbolic/sym_state_space_manager.h +@@ -135,19 +135,16 @@ public: + // Methods that require of TRs initialized + + int getMinTransitionCost() const { +- assert(!transitions.empty()); + return min_transition_cost; + } + + int getAbsoluteMinTransitionCost() const { +- assert(!transitions.empty()); + if (hasTR0) + return 0; + return min_transition_cost; + } + + bool hasTransitions0() const { +- assert(!transitions.empty()); + return hasTR0; + } + +diff --git a/src/search/symbolic/sym_utils.cc b/src/search/symbolic/sym_utils.cc +index b86b30a9b..6d80db0de 100644 +--- a/src/search/symbolic/sym_utils.cc ++++ b/src/search/symbolic/sym_utils.cc +@@ -1,5 +1,7 @@ + #include "sym_utils.h" + ++using namespace std; ++ + namespace symbolic { + TransitionRelation mergeTR(TransitionRelation tr, const TransitionRelation &tr2, + int maxSize) { +@@ -13,4 +15,32 @@ BDD mergeAndBDD(const BDD &bdd, const BDD &bdd2, int maxSize) { + BDD mergeOrBDD(const BDD &bdd, const BDD &bdd2, int maxSize) { + return bdd.Or(bdd2, maxSize); + } ++ ++void partition_add_to_bdds(SymVariables *vars, ADD add, map &res) { ++ assert(res.empty()); ++ ADD cur_add = add; ++ double min_value = Cudd_V(cur_add.FindMin().getNode()); ++ ++ ADD inf = vars->constant(numeric_limits::infinity()); ++ while (cur_add != inf) { ++ res[min_value] = cur_add.BddInterval(min_value, min_value); ++ cur_add = cur_add.Maximum(res[min_value].Add() * inf); ++ min_value = Cudd_V(cur_add.FindMin().getNode()); ++ } ++} ++ ++void partition_add_to_bdds(SymVariables *vars, ADD add, map &res) { ++ assert(res.empty()); ++ ADD cur_add = add; ++ double min_value = Cudd_V(cur_add.FindMin().getNode()); ++ ++ ADD inf = vars->constant(numeric_limits::infinity()); ++ while (cur_add != inf) { ++ int int_min_value = round(min_value); ++ res[int_min_value] = cur_add.BddInterval(min_value, min_value); ++ // vars->to_dot(res[int_min_value], "bdd_" + to_string(int_min_value) + "_util.dot"); ++ cur_add = cur_add + (res[int_min_value].Add() * inf); ++ min_value = Cudd_V(cur_add.FindMin().getNode()); ++ } ++} + } +diff --git a/src/search/symbolic/sym_utils.h b/src/search/symbolic/sym_utils.h +index 0c1f67095..52cb3574b 100644 +--- a/src/search/symbolic/sym_utils.h ++++ b/src/search/symbolic/sym_utils.h +@@ -130,5 +130,8 @@ BDD mergeAndBDD(const BDD &bdd, const BDD &bdd2, int maxSize); + BDD mergeOrBDD(const BDD &bdd, const BDD &bdd2, int maxSize); + + inline std::string dirname(bool fw) {return fw ? "fw" : "bw";} ++ ++void partition_add_to_bdds(SymVariables *vars, ADD add, std::map &res); ++void partition_add_to_bdds(SymVariables *vars, ADD add, std::map &res); + } + #endif +diff --git a/src/search/task_proxy.h b/src/search/task_proxy.h +index 58ac4b746..6f60d735d 100644 +--- a/src/search/task_proxy.h ++++ b/src/search/task_proxy.h +@@ -32,6 +32,8 @@ class PreconditionsProxy; + class State; + class StateRegistry; + class TaskProxy; ++class UtilitiesProxy; ++class UtilityProxy; + class VariableProxy; + class VariablesProxy; + +@@ -553,6 +555,72 @@ public: + } + }; + ++class UtilityProxy { ++ const AbstractTask *task; ++ FactPair fact; ++ int util; ++public: ++ UtilityProxy(const AbstractTask &task, int var_id, int var_val, int util); ++ UtilityProxy(const AbstractTask &task, const std::pair &util); ++ ~UtilityProxy() = default; ++ ++ VariableProxy get_variable() const { ++ return VariableProxy(*task, fact.var); ++ } ++ ++ int get_var_value() const { ++ return fact.value; ++ } ++ ++ FactPair get_fact_pair() const { ++ return fact; ++ } ++ ++ std::string get_fact_name() const { ++ return task->get_fact_name(fact); ++ } ++ ++ int get_utility() const { ++ return util; ++ } ++ ++ bool operator==(const UtilityProxy &other) const { ++ assert(task == other.task); ++ return fact == other.fact && util == other.util; ++ } ++ ++ bool operator!=(const UtilityProxy &other) const { ++ return !(*this == other); ++ } ++ ++ bool is_mutex(const UtilityProxy &other) const { ++ return task->are_facts_mutex(fact, other.fact); ++ } ++}; ++ ++class UtilitiesProxy { ++protected: ++ const AbstractTask *task; ++public: ++ using ItemType = UtilityProxy; ++ explicit UtilitiesProxy(const AbstractTask &task) ++ : task(&task) {} ++ virtual ~UtilitiesProxy() = default; ++ ++ virtual std::size_t size() const { ++ return task->get_num_utilties(); ++ } ++ ++ virtual UtilityProxy operator[](std::size_t index) const { ++ assert(index < size()); ++ return UtilityProxy(*task, task->get_utility(index)); ++ } ++ ++ bool empty() const { ++ return size() == 0; ++ } ++}; ++ + + bool does_fire(const EffectProxy &effect, const State &state); + +@@ -685,6 +753,18 @@ public: + return GoalsProxy(*task); + } + ++ UtilitiesProxy get_utilities() const { ++ return UtilitiesProxy(*task); ++ } ++ ++ int get_constant_utility() const { ++ return task->get_constant_utility(); ++ } ++ ++ int get_plan_cost_bound() const { ++ return task->get_plan_cost_bound(); ++ } ++ + State create_state(std::vector &&state_values) const { + return State(*task, std::move(state_values)); + } +@@ -742,6 +822,16 @@ inline FactProxy::FactProxy(const AbstractTask &task, int var_id, int value) + : FactProxy(task, FactPair(var_id, value)) { + } + ++inline UtilityProxy::UtilityProxy(const AbstractTask &task, int var_id, int var_val, int util) ++ : task(&task), fact(var_id, var_val), util(util) { ++ assert(fact.var >= 0 && fact.var < task.get_num_variables()); ++ assert(fact.value >= 0 && fact.value < get_variable().get_domain_size()); ++} ++ ++inline UtilityProxy::UtilityProxy(const AbstractTask &task, const std::pair &util) ++ : UtilityProxy(task, util.first.var, util.first.value, util.second) { ++} ++ + + inline VariableProxy FactProxy::get_variable() const { + return VariableProxy(*task, fact.var); +diff --git a/src/search/task_utils/task_properties.cc b/src/search/task_utils/task_properties.cc +index 85ae16543..dddbc2af7 100644 +--- a/src/search/task_utils/task_properties.cc ++++ b/src/search/task_utils/task_properties.cc +@@ -13,6 +13,11 @@ using utils::ExitCode; + + + namespace task_properties { ++bool is_oversubscribed(TaskProxy task) { ++ // If the plan cost bound is -1 we have a classical task ++ return task.get_plan_cost_bound() != -1; ++} ++ + bool is_unit_cost(TaskProxy task) { + for (OperatorProxy op : task.get_operators()) { + if (op.get_cost() != 1) +diff --git a/src/search/task_utils/task_properties.h b/src/search/task_utils/task_properties.h +index f53d34997..e23ed18ea 100644 +--- a/src/search/task_utils/task_properties.h ++++ b/src/search/task_utils/task_properties.h +@@ -23,6 +23,13 @@ inline bool is_goal_state(TaskProxy task, const State &state) { + return true; + } + ++/* ++ Return true if utilites and a cost bound are present ++ Runtime: O(1) ++*/ ++ ++extern bool is_oversubscribed(TaskProxy task); ++ + /* + Return true iff all operators have cost 1. + +diff --git a/src/search/tasks/delegating_task.cc b/src/search/tasks/delegating_task.cc +index 0cedb705e..373fc75b5 100644 +--- a/src/search/tasks/delegating_task.cc ++++ b/src/search/tasks/delegating_task.cc +@@ -108,6 +108,22 @@ vector DelegatingTask::get_mutex_groups() const { + return parent->get_mutex_groups(); + } + ++int DelegatingTask::get_num_utilties() const { ++ return parent->get_num_utilties(); ++} ++ ++pair DelegatingTask::get_utility(int index) const { ++ return parent->get_utility(index); ++} ++ ++int DelegatingTask::get_constant_utility() const { ++ return parent->get_constant_utility(); ++} ++ ++int DelegatingTask::get_plan_cost_bound() const { ++ return parent->get_plan_cost_bound(); ++} ++ + void DelegatingTask::convert_ancestor_state_values( + vector &values, const AbstractTask *ancestor_task) const { + if (this == ancestor_task) { +diff --git a/src/search/tasks/delegating_task.h b/src/search/tasks/delegating_task.h +index 31a52e020..7a06d6da1 100644 +--- a/src/search/tasks/delegating_task.h ++++ b/src/search/tasks/delegating_task.h +@@ -3,6 +3,7 @@ + + #include "../abstract_task.h" + ++#include + #include + #include + #include +@@ -58,6 +59,11 @@ public: + virtual std::vector get_initial_state_values() const override; + virtual std::vector get_mutex_groups() const override; + ++ virtual int get_num_utilties() const override; ++ virtual std::pair get_utility(int index) const override; ++ virtual int get_constant_utility() const override; ++ virtual int get_plan_cost_bound() const override; ++ + virtual void convert_ancestor_state_values( + std::vector &values, + const AbstractTask *ancestor_task) const final override; +diff --git a/src/search/tasks/root_task.cc b/src/search/tasks/root_task.cc +index af9efb4ef..85d7c007d 100644 +--- a/src/search/tasks/root_task.cc ++++ b/src/search/tasks/root_task.cc +@@ -9,6 +9,7 @@ + + #include + #include ++#include + #include + #include + #include +@@ -64,6 +65,11 @@ class RootTask : public AbstractTask { + vector initial_state_values; + vector goals; + ++ // OSP data ++ vector> utilities; ++ int constant_utility; ++ int plan_cost_bound; ++ + const ExplicitVariable &get_variable(int var) const; + const ExplicitEffect &get_effect(int op_id, int effect_id, bool is_axiom) const; + const ExplicitOperator &get_operator_or_axiom(int index, bool is_axiom) const; +@@ -107,6 +113,10 @@ public: + + virtual vector get_initial_state_values() const override; + virtual vector get_mutex_groups() const override; ++ virtual int get_num_utilties() const override; ++ virtual pair get_utility(int index) const override; ++ virtual int get_constant_utility() const override; ++ virtual int get_plan_cost_bound() const override; + virtual void convert_ancestor_state_values( + vector &values, + const AbstractTask *ancestor_task) const override; +@@ -333,11 +343,60 @@ vector read_goal(istream &in) { + check_magic(in, "begin_goal"); + vector goals = read_facts(in); + check_magic(in, "end_goal"); +- if (goals.empty()) { +- cerr << "Task has no goal condition!" << endl; ++ return goals; ++} ++ ++vector> read_utilities(istream &in) { ++ string word; ++ in >> word; ++ vector> utilities; ++ if (word != "begin_util") { ++ // set pointer back ++ in.seekg(-word.length(), ios::cur); ++ return utilities; ++ } ++ int count; ++ in >> count; ++ for (int i = 0; i < count; i++) { ++ FactPair condition = FactPair::no_fact; ++ int util; ++ in >> condition.var >> condition.value >> util; ++ utilities.emplace_back(condition, util); ++ } ++ check_magic(in, "end_util"); ++ return utilities; ++} ++ ++int read_constant_utility(istream &in) { ++ string word; ++ in >> word; ++ int constant_util = -1; ++ if (word != "begin_constant_util") { ++ // set pointer back ++ in.seekg(-word.length(), ios::cur); ++ return 0; ++ } ++ in >> constant_util; ++ check_magic(in, "end_constant_util"); ++ return constant_util; // we use strictly smaller ++} ++ ++int read_plan_cost_bound(istream &in) { ++ string word; ++ in >> word; ++ int bound = -1; ++ if (word != "begin_bound") { ++ // set pointer back ++ in.seekg(-word.length(), ios::cur); ++ return -1; ++ } ++ in >> bound; ++ if (bound < 0) { ++ cerr << "Task has negative cost bound!" << endl; + utils::exit_with(ExitCode::SEARCH_INPUT_ERROR); + } +- return goals; ++ check_magic(in, "end_bound"); ++ return bound + 1; // we use strictly smaller + } + + vector read_actions( +@@ -376,6 +435,21 @@ RootTask::RootTask(istream &in) { + + goals = read_goal(in); + check_facts(goals, variables); ++ ++ // OSP ++ utilities = read_utilities(in); ++ for (const pair &util : utilities) { ++ check_fact(util.first, variables); ++ } ++ constant_utility = read_constant_utility(in); ++ plan_cost_bound = read_plan_cost_bound(in); ++ ++ // No OSP task and empty goal ++ if (plan_cost_bound < 0 && goals.empty()) { ++ cerr << "Task has no goal condition!" << endl; ++ utils::exit_with(ExitCode::SEARCH_INPUT_ERROR); ++ } ++ + operators = read_actions(in, false, use_metric, variables); + axioms = read_actions(in, true, use_metric, variables); + /* TODO: We should be stricter here and verify that we +@@ -524,6 +598,22 @@ vector RootTask::get_mutex_groups() const { + return mutex_groups; + } + ++int RootTask::get_num_utilties() const { ++ return utilities.size(); ++} ++ ++pair RootTask::get_utility(int index) const { ++ return utilities[index]; ++} ++ ++int RootTask::get_constant_utility() const { ++ return constant_utility; ++} ++ ++int RootTask::get_plan_cost_bound() const { ++ return plan_cost_bound; ++} ++ + void RootTask::convert_ancestor_state_values( + vector &, const AbstractTask *ancestor_task) const { + if (this != ancestor_task) { +diff --git a/src/translate/normalize.py b/src/translate/normalize.py +index 375dc67e9..00793a743 100755 +--- a/src/translate/normalize.py ++++ b/src/translate/normalize.py +@@ -310,6 +310,8 @@ def eliminate_existential_quantifiers_from_conditional_effects(task): + + def substitute_complicated_goal(task): + goal = task.goal ++ if isinstance(goal, pddl.Truth): ++ return + if isinstance(goal, pddl.Literal): + return + elif isinstance(goal, pddl.Conjunction): +diff --git a/src/translate/pddl/tasks.py b/src/translate/pddl/tasks.py +index 2dd6073f5..bcbbe7ce0 100644 +--- a/src/translate/pddl/tasks.py ++++ b/src/translate/pddl/tasks.py +@@ -5,6 +5,7 @@ from . import predicates + class Task: + def __init__(self, domain_name, task_name, requirements, + types, objects, predicates, functions, init, goal, ++ utility, bound, + actions, axioms, use_metric): + self.domain_name = domain_name + self.task_name = task_name +@@ -15,11 +16,16 @@ class Task: + self.functions = functions + self.init = init + self.goal = goal ++ self.utility = utility ++ self.bound = bound + self.actions = actions + self.axioms = axioms + self.axiom_counter = 0 + self.use_min_cost_metric = use_metric + ++ def is_osp_task(self): ++ return self.bound is not None ++ + def add_axiom(self, parameters, condition): + name = "new-axiom@%d" % self.axiom_counter + self.axiom_counter += 1 +@@ -48,6 +54,12 @@ class Task: + print(" %s" % fact) + print("Goal:") + self.goal.dump() ++ if self.is_osp_task(): ++ print("Utility:") ++ for u in self.utility: ++ print(f" {u[0]}: {u[1]}") ++ print("Bound:") ++ print(f" {self.bound}") + print("Actions:") + for action in self.actions: + action.dump() +diff --git a/src/translate/pddl_parser/parsing_functions.py b/src/translate/pddl_parser/parsing_functions.py +index fdc0b9dc8..2852dc8a4 100644 +--- a/src/translate/pddl_parser/parsing_functions.py ++++ b/src/translate/pddl_parser/parsing_functions.py +@@ -295,9 +295,13 @@ def parse_axiom(alist, type_dict, predicate_dict): + def parse_task(domain_pddl, task_pddl): + domain_name, domain_requirements, types, type_dict, constants, predicates, predicate_dict, functions, actions, axioms \ + = parse_domain_pddl(domain_pddl) +- task_name, task_domain_name, task_requirements, objects, init, goal, use_metric = parse_task_pddl(task_pddl, type_dict, predicate_dict) +- ++ task_name, task_domain_name, task_requirements, objects, init, goal, utility, bound, use_metric = parse_task_pddl(task_pddl, type_dict, predicate_dict) + assert domain_name == task_domain_name ++ ++ # Ensure that we have either a hard goal (classical task) or a soft goal (osp task) ++ assert ( ++ (goal != pddl.Truth() and not utility and not bound) or goal == pddl.Truth() and utility and bound ++ ), "We currently only support specifying a classical planning task with a hard goal using :goal, or specifying an oversubscription planning task using :utility and :bound, but not a combination." + requirements = pddl.Requirements(sorted(set( + domain_requirements.requirements + + task_requirements.requirements))) +@@ -310,7 +314,7 @@ def parse_task(domain_pddl, task_pddl): + + return pddl.Task( + domain_name, task_name, requirements, types, objects, +- predicates, functions, init, goal, actions, axioms, use_metric) ++ predicates, functions, init, goal, utility, bound, actions, axioms, use_metric) + + + def parse_domain_pddl(domain_pddl): +@@ -460,8 +464,35 @@ def parse_task_pddl(task_pddl, type_dict, predicate_dict): + yield initial + + goal = next(iterator) +- assert goal[0] == ":goal" and len(goal) == 2 +- yield parse_condition(goal[1], type_dict, predicate_dict) ++ goal_cond = pddl.Truth() ++ ++ if goal[0] == ":goal": ++ if len(goal) == 2: ++ goal_cond = parse_condition(goal[1], type_dict, predicate_dict) ++ utility = next(iterator, None) ++ else: ++ utility = goal ++ ++ yield goal_cond ++ ++ if utility and utility[0] == ":utility": ++ assert utility[0] == ":utility" ++ utility_list = [] ++ for fact in utility[1:]: ++ assert fact[0] == "=" ++ utility_atom = pddl.Atom(fact[1][0], fact[1][1:]) ++ utility_value = fact[2] ++ utility_list.append((utility_atom, utility_value)) ++ yield utility_list ++ else: ++ yield None ++ ++ bound = next(iterator, None) ++ if bound and bound[0] == ":bound": ++ assert bound[0] == ":bound" and len(bound) == 2 ++ yield bound[1] ++ else: ++ yield None + + use_metric = False + for entry in iterator: +@@ -470,6 +501,8 @@ def parse_task_pddl(task_pddl, type_dict, predicate_dict): + use_metric = True + else: + assert False, "Unknown metric." ++ elif entry[0] == ":use-cost-metric": ++ use_metric = True + yield use_metric + + for entry in iterator: +diff --git a/src/translate/sas_tasks.py b/src/translate/sas_tasks.py +index 14db935fa..081218b35 100644 +--- a/src/translate/sas_tasks.py ++++ b/src/translate/sas_tasks.py +@@ -11,12 +11,15 @@ class SASTask: + generally be sorted and mention each variable at most once. See + the validate methods for details.""" + +- def __init__(self, variables, mutexes, init, goal, ++ def __init__(self, variables, mutexes, init, goal, utility, constant_utility, bound, + operators, axioms, metric): + self.variables = variables + self.mutexes = mutexes + self.init = init + self.goal = goal ++ self.utility = utility ++ self.constant_utility = constant_utility ++ self.bound = bound + self.operators = sorted(operators, key=lambda op: ( + op.name, op.prevail, op.pre_post)) + self.axioms = sorted(axioms, key=lambda axiom: ( +@@ -25,6 +28,9 @@ class SASTask: + if DEBUG: + self.validate() + ++ def is_osp_task(self): ++ return self.bound is not None ++ + def validate(self): + """Fail an assertion if the task is invalid. + +@@ -50,6 +56,7 @@ class SASTask: + mutex.validate(self.variables) + self.init.validate(self.variables) + self.goal.validate(self.variables) ++ self.utility.validate() + for op in self.operators: + op.validate(self.variables) + for axiom in self.axioms: +@@ -67,6 +74,13 @@ class SASTask: + self.init.dump() + print("goal:") + self.goal.dump() ++ if self.is_osp_task(): ++ print("utility:") ++ self.utility.dump() ++ print("constant_utility") ++ print(" %d" % self.constant_utility) ++ print("bound:") ++ print(" %d" % self.bound) + print("%d operators:" % len(self.operators)) + for operator in self.operators: + operator.dump() +@@ -88,6 +102,14 @@ class SASTask: + mutex.output(stream) + self.init.output(stream) + self.goal.output(stream) ++ if self.is_osp_task(): ++ self.utility.output(stream) ++ print("begin_constant_util", file=stream) ++ print(self.constant_utility, file=stream) ++ print("end_constant_util", file=stream) ++ print("begin_bound", file=stream) ++ print(self.bound, file=stream) ++ print("end_bound", file=stream) + print(len(self.operators), file=stream) + for op in self.operators: + op.output(stream) +@@ -101,6 +123,7 @@ class SASTask: + for mutex in self.mutexes: + task_size += mutex.get_encoding_size() + task_size += self.goal.get_encoding_size() ++ task_size += self.utility.get_encoding_size() + for op in self.operators: + task_size += op.get_encoding_size() + for axiom in self.axioms: +@@ -226,6 +249,32 @@ class SASInit: + print("end_state", file=stream) + + ++class SASUtil: ++ def __init__(self, values): ++ self.triplets = sorted(values) ++ ++ def __repr__(self) -> str: ++ return str(self.triplets) ++ ++ def validate(self): ++ """ Assert that the utility is not empty.""" ++ assert self.triplets ++ ++ def dump(self): ++ for var, val, u in self.triplets: ++ print("v%d: %d = %d" % (var, val, u)) ++ ++ def output(self, stream): ++ print("begin_util", file=stream) ++ print(len(self.triplets), file=stream) ++ for var, val, u in self.triplets: ++ print(var, val, u, file=stream) ++ print("end_util", file=stream) ++ ++ def get_encoding_size(self): ++ return len(self.triplets) ++ ++ + class SASGoal: + def __init__(self, pairs): + self.pairs = sorted(pairs) +@@ -264,6 +313,7 @@ class SASOperator: + def tuplify(entry): + var, pre, post, cond = entry + return var, pre, post, tuple(cond) ++ + def listify(entry): + var, pre, post, cond = entry + return var, pre, post, list(cond) +@@ -407,7 +457,6 @@ class SASAxiom: + assert val >= 0, condition + + def validate(self, variables, init): +- + """Validate the axiom. + + Assert that the axiom condition is a valid condition, that the +@@ -439,8 +488,8 @@ class SASAxiom: + eff_layer = variables.axiom_layers[eff_var] + assert eff_layer >= 0 + eff_init_value = init.values[eff_var] +- ## The following rule is currently commented out because of +- ## the TODO/bug mentioned in the docstring. ++ # The following rule is currently commented out because of ++ # the TODO/bug mentioned in the docstring. + # assert eff_value != eff_init_value + for cond_var, cond_value in self.condition: + cond_layer = variables.axiom_layers[cond_var] +@@ -448,9 +497,9 @@ class SASAxiom: + assert cond_layer <= eff_layer + if cond_layer == eff_layer: + cond_init_value = init.values[cond_var] +- ## Once the TODO/bug above is addressed, the +- ## following four lines can be simplified because +- ## we are guaranteed to land in the "if" branch. ++ # Once the TODO/bug above is addressed, the ++ # following four lines can be simplified because ++ # we are guaranteed to land in the "if" branch. + if eff_value != eff_init_value: + assert cond_value != cond_init_value + else: +diff --git a/src/translate/simplify.py b/src/translate/simplify.py +index 43236814b..4934568e2 100644 +--- a/src/translate/simplify.py ++++ b/src/translate/simplify.py +@@ -156,15 +156,19 @@ def build_dtgs(task): + always_false = object() + always_true = object() + ++ + class Impossible(Exception): + pass + ++ + class TriviallySolvable(Exception): + pass + ++ + class DoesNothing(Exception): + pass + ++ + class VarValueRenaming: + def __init__(self): + self.new_var_nos = [] # indexed by old var_no +@@ -230,9 +234,21 @@ class VarValueRenaming: + self.apply_to_mutexes(task.mutexes) + self.apply_to_init(task.init) + self.apply_to_goals(task.goal.pairs) ++ self.apply_to_utils(task) ++ if task.is_osp_task() and self.is_trivial_unsolvable(task.utility.triplets): ++ raise TriviallySolvable ++ elif not task.is_osp_task() and self.is_trivial_unsolvable(task.goal.pairs): ++ raise TriviallySolvable + self.apply_to_operators(task.operators) + self.apply_to_axioms(task.axioms) + ++ def is_trivial_unsolvable(self, goals): ++ # We raise an exception because we do not consider a SAS+ ++ # task without goals well-formed. Our callers are supposed ++ # to catch this and replace the task with a well-formed ++ # trivially solvable task. ++ return not goals ++ + def apply_to_variables(self, variables): + variables.ranges = self.new_sizes + new_axiom_layers = [None] * self.new_var_count +@@ -256,7 +272,8 @@ class VarValueRenaming: + print("Removed false proposition: %s" % value_name) + else: + new_value_names[new_var_no][new_value] = value_name +- assert all((None not in value_names) for value_names in new_value_names) ++ assert all((None not in value_names) ++ for value_names in new_value_names) + value_names[:] = new_value_names + + def apply_to_mutexes(self, mutexes): +@@ -266,7 +283,7 @@ class VarValueRenaming: + for var, val in mutex.facts: + new_var_no, new_value = self.translate_pair((var, val)) + if (new_value is not always_true and +- new_value is not always_false): ++ new_value is not always_false): + new_facts.append((new_var_no, new_value)) + if len(new_facts) >= 2: + mutex.facts = new_facts +@@ -288,12 +305,11 @@ class VarValueRenaming: + def apply_to_goals(self, goals): + # This may propagate Impossible up. + self.convert_pairs(goals) +- if not goals: +- # We raise an exception because we do not consider a SAS+ +- # task without goals well-formed. Our callers are supposed +- # to catch this and replace the task with a well-formed +- # trivially solvable task. +- raise TriviallySolvable ++ ++ def apply_to_utils(self, task): ++ # This may propagate Impossible up. ++ if task.is_osp_task(): ++ task.constant_utility += self.convert_utility_triplets(task.utility.triplets) + + def apply_to_operators(self, operators): + new_operators = [] +@@ -442,7 +458,7 @@ class VarValueRenaming: + + for cond_var, cond_value in new_cond: + if (cond_var in conditions_dict and +- conditions_dict[cond_var] != cond_value): ++ conditions_dict[cond_var] != cond_value): + # This effect condition is not compatible with + # the applicability conditions. + return None +@@ -476,6 +492,34 @@ class VarValueRenaming: + new_pairs.append((new_var_no, new_value)) + pairs[:] = new_pairs + ++ def translate_triplet(self, util_triplet): ++ # print(util_triplet) ++ (var_no, value, uval) = util_triplet ++ new_var_no = self.new_var_nos[var_no] ++ new_value = self.new_values[var_no][value] ++ return new_var_no, new_value, uval ++ ++ def convert_utility_triplets(self, util_triplets): ++ # We call this convert_... because it is an in-place method. ++ constant_util = 0 ++ new_triplets = [] ++ for tr in util_triplets: ++ new_var_no, new_value, uval = self.translate_triplet(tr) ++ if new_value is always_false: ++ print(f"{tr} pruned because it is always false.") ++ # This is not valid for soft goal problems. ++ # raise Impossible ++ continue ++ elif new_value is always_true: ++ print(f"{tr} pruned because it is always true.") ++ constant_util += uval ++ else: ++ assert new_var_no is not None ++ new_triplets.append((new_var_no, new_value, uval)) ++ util_triplets[:] = new_triplets ++ return constant_util ++ ++ + def build_renaming(dtgs): + renaming = VarValueRenaming() + for dtg in dtgs: +diff --git a/src/translate/translate.py b/src/translate/translate.py +index f42a06453..1e406f61f 100755 +--- a/src/translate/translate.py ++++ b/src/translate/translate.py +@@ -1,37 +1,37 @@ + #! /usr/bin/env python3 + + ++import variable_order ++import tools ++import timers ++import simplify ++import signal ++import sas_tasks ++import pddl_parser ++import pddl ++import options ++import normalize ++import instantiate ++import fact_groups ++import axiom_rules ++from itertools import product ++from copy import deepcopy ++from collections import defaultdict + import os + import sys + import traceback + ++ + def python_version_supported(): + return sys.version_info >= (3, 6) + ++ + if not python_version_supported(): + sys.exit("Error: Translator only supports Python >= 3.6.") + + +-from collections import defaultdict +-from copy import deepcopy +-from itertools import product +- +-import axiom_rules +-import fact_groups +-import instantiate +-import normalize +-import options +-import pddl +-import pddl_parser +-import sas_tasks +-import signal +-import simplify +-import timers +-import tools +-import variable_order +- + # TODO: The translator may generate trivial derived variables which are always +-# true, for example if there ia a derived predicate in the input that only ++# true, for example if there is a derived predicate in the input that only + # depends on (non-derived) variables which are detected as always true. + # Such a situation was encountered in the PSR-STRIPS-DerivedPredicates domain. + # Such "always-true" variables should best be compiled away, but it is +@@ -43,8 +43,8 @@ import variable_order + DEBUG = False + + +-## For a full list of exit codes, please see driver/returncodes.py. Here, +-## we only list codes that are used by the translator component of the planner. ++# For a full list of exit codes, please see driver/returncodes.py. Here, ++# we only list codes that are used by the translator component of the planner. + TRANSLATE_OUT_OF_MEMORY = 20 + TRANSLATE_OUT_OF_TIME = 21 + +@@ -93,19 +93,19 @@ def translate_strips_conditions_aux(conditions, dictionary, ranges): + + for fact in conditions: + if fact.negated: +- ## Note: here we use a different solution than in Sec. 10.6.4 +- ## of the thesis. Compare the last sentences of the third +- ## paragraph of the section. +- ## We could do what is written there. As a test case, +- ## consider Airport ADL tasks with only one airport, where +- ## (occupied ?x) variables are encoded in a single variable, +- ## and conditions like (not (occupied ?x)) do occur in +- ## preconditions. +- ## However, here we avoid introducing new derived predicates +- ## by treating the negative precondition as a disjunctive +- ## precondition and expanding it by "multiplying out" the +- ## possibilities. This can lead to an exponential blow-up so +- ## it would be nice to choose the behaviour as an option. ++ # Note: here we use a different solution than in Sec. 10.6.4 ++ # of the thesis. Compare the last sentences of the third ++ # paragraph of the section. ++ # We could do what is written there. As a test case, ++ # consider Airport ADL tasks with only one airport, where ++ # (occupied ?x) variables are encoded in a single variable, ++ # and conditions like (not (occupied ?x)) do occur in ++ # preconditions. ++ # However, here we avoid introducing new derived predicates ++ # by treating the negative precondition as a disjunctive ++ # precondition and expanding it by "multiplying out" the ++ # possibilities. This can lead to an exponential blow-up so ++ # it would be nice to choose the behaviour as an option. + done = False + new_condition = {} + atom = pddl.Atom(fact.predicate, fact.args) # force positive +@@ -133,7 +133,8 @@ def translate_strips_conditions_aux(conditions, dictionary, ranges): + # this atom. So we need to introduce a new condition: + # We can select any from new_condition and currently prefer the + # smallest one. +- candidates = sorted(new_condition.items(), key=number_of_values) ++ candidates = sorted(new_condition.items(), ++ key=number_of_values) + var, vals = candidates[0] + condition[var] = vals + +@@ -268,7 +269,8 @@ def translate_strips_operator_aux(operator, dictionary, ranges, mutex_dict, + break + new_cond[cvar] = cval + else: +- effects_by_variable[var][none_of_those].append(new_cond) ++ effects_by_variable[var][none_of_those].append( ++ new_cond) + + return build_sas_operator(operator.name, condition, effects_by_variable, + operator.cost, ranges, implied_facts) +@@ -332,21 +334,21 @@ def build_sas_operator(name, condition, effects_by_variable, cost, ranges, + + + def prune_stupid_effect_conditions(var, val, conditions, effects_on_var): +- ## (IF THEN := ) is a conditional effect. +- ## is guaranteed to be a binary variable. +- ## is in DNF representation (list of lists). ++ # (IF THEN := ) is a conditional effect. ++ # is guaranteed to be a binary variable. ++ # is in DNF representation (list of lists). + ## +- ## We simplify by applying two rules: +- ## 1. Conditions of the form "var = dualval" where var is the +- ## effect variable and dualval != val can be omitted. +- ## (If var != dualval, then var == val because it is binary, +- ## which means that in such situations the effect is a no-op.) +- ## The condition can only be omitted if there is no effect +- ## producing dualval (see issue736). +- ## 2. If conditions contains any empty list, it is equivalent +- ## to True and we can remove all other disjuncts. ++ # We simplify by applying two rules: ++ # 1. Conditions of the form "var = dualval" where var is the ++ # effect variable and dualval != val can be omitted. ++ # (If var != dualval, then var == val because it is binary, ++ # which means that in such situations the effect is a no-op.) ++ # The condition can only be omitted if there is no effect ++ # producing dualval (see issue736). ++ # 2. If conditions contains any empty list, it is equivalent ++ # to True and we can remove all other disjuncts. + ## +- ## returns True when anything was changed ++ # returns True when anything was changed + if conditions == [[]]: + return False # Quick exit for common case. + assert val in [0, 1] +@@ -405,7 +407,7 @@ def translate_strips_axioms(axioms, strips_to_sas, ranges, mutex_dict, + return result + + +-def dump_task(init, goals, actions, axioms, axiom_layer_dict): ++def dump_task(init, goals, utils, bound, actions, axioms, axiom_layer_dict): + old_stdout = sys.stdout + with open("output.dump", "w") as dump_file: + sys.stdout = dump_file +@@ -413,9 +415,17 @@ def dump_task(init, goals, actions, axioms, axiom_layer_dict): + for atom in init: + print(atom) + print() +- print("Goals") +- for goal in goals: +- print(goal) ++ if not utils: ++ print("Goals") ++ for goal in goals: ++ print(goal) ++ else: ++ print("Utils") ++ if utils: ++ for util in utils: ++ print(util) ++ print("Bound") ++ print(" %s" % bound) + for action in actions: + print() + print("Action") +@@ -433,7 +443,7 @@ def dump_task(init, goals, actions, axioms, axiom_layer_dict): + + def translate_task(strips_to_sas, ranges, translation_key, + mutex_dict, mutex_ranges, mutex_key, +- init, goals, ++ init, goals, utilities, bound, + actions, axioms, metric, implied_facts): + with timers.timing("Processing axioms", block=True): + axioms, axiom_layer_dict = axiom_rules.handle_axioms(actions, axioms, goals, +@@ -442,12 +452,21 @@ def translate_task(strips_to_sas, ranges, translation_key, + if options.dump_task: + # Remove init facts that don't occur in strips_to_sas: they're constant. + nonconstant_init = filter(strips_to_sas.get, init) +- dump_task(nonconstant_init, goals, actions, axioms, axiom_layer_dict) ++ dump_task(nonconstant_init, goals, utilities, ++ bound, actions, axioms, axiom_layer_dict) + ++ constant_utility = 0 + init_values = [rang - 1 for rang in ranges] + # Closed World Assumption: Initialize to "range - 1" == Nothing. + for fact in init: + pairs = strips_to_sas.get(fact, []) # empty for static init facts ++ ++ # Osp task and this fact has a base utility ++ if bound is not None and len(pairs) == 0 and isinstance(fact, pddl.conditions.Atom): ++ for util in utilities: ++ if util[0] == fact: ++ print(f"{fact} is constant with utlity {util[1]}.") ++ constant_utility += int(util[1]) + for var, val in pairs: + curr_val = init_values[var] + if curr_val != ranges[var] - 1 and curr_val != val: +@@ -455,24 +474,36 @@ def translate_task(strips_to_sas, ranges, translation_key, + init_values[var] = val + init = sas_tasks.SASInit(init_values) + +- goal_dict_list = translate_strips_conditions(goals, strips_to_sas, ranges, +- mutex_dict, mutex_ranges) +- if goal_dict_list is None: +- # "None" is a signal that the goal is unreachable because it +- # violates a mutex. +- return unsolvable_sas_task("Goal violates a mutex") +- +- assert len(goal_dict_list) == 1, "Negative goal not supported" +- ## we could substitute the negative goal literal in +- ## normalize.substitute_complicated_goal, using an axiom. We currently +- ## don't do this, because we don't run into this assertion, if the +- ## negative goal is part of finite domain variable with only two +- ## values, which is most of the time the case, and hence refrain from +- ## introducing axioms (that are not supported by all heuristics) +- goal_pairs = list(goal_dict_list[0].items()) +- if not goal_pairs: +- return solvable_sas_task("Empty goal") +- goal = sas_tasks.SASGoal(goal_pairs) ++ util_values = [] ++ if utilities: ++ goal = sas_tasks.SASGoal([]) ++ for fact, uval in utilities: ++ pairs = strips_to_sas.get(fact, []) ++ for var, val in pairs: ++ util_values.append((var, val, int(uval))) ++ else: ++ goal_dict_list = translate_strips_conditions(goals, strips_to_sas, ranges, ++ mutex_dict, mutex_ranges) ++ ++ if goal_dict_list is None: ++ # "None" is a signal that the goal is unreachable because it ++ # violates a mutex. ++ return unsolvable_sas_task("Goal violates a mutex", None) ++ ++ assert len(goal_dict_list) == 1, "Negative goal not supported" ++ # we could substitute the negative goal literal in ++ # normalize.substitute_complicated_goal, using an axiom. We currently ++ # don't do this, because we don't run into this assertion, if the ++ # negative goal is part of finite domain variable with only two ++ # values, which is most of the time the case, and hence refrain from ++ # introducing axioms (that are not supported by all heuristics) ++ goal_pairs = list(goal_dict_list[0].items()) ++ if not goal_pairs: ++ return solvable_sas_task("Empty goal", None) ++ goal = sas_tasks.SASGoal(goal_pairs) ++ ++ util = sas_tasks.SASUtil(util_values) ++ bound = int(bound) if bound else None + + operators = translate_strips_operators(actions, strips_to_sas, ranges, + mutex_dict, mutex_ranges, +@@ -487,11 +518,11 @@ def translate_task(strips_to_sas, ranges, translation_key, + axiom_layers[var] = layer + variables = sas_tasks.SASVariables(ranges, axiom_layers, translation_key) + mutexes = [sas_tasks.SASMutexGroup(group) for group in mutex_key] +- return sas_tasks.SASTask(variables, mutexes, init, goal, ++ return sas_tasks.SASTask(variables, mutexes, init, goal, util, constant_utility, bound, + operators, axioms, metric) + + +-def trivial_task(solvable): ++def trivial_task(solvable, task): + variables = sas_tasks.SASVariables( + [2], [-1], [["Atom dummy(val1)", "Atom dummy(val2)"]]) + # We create no mutexes: the only possible mutex is between +@@ -500,34 +531,57 @@ def trivial_task(solvable): + # finite-domain variable). + mutexes = [] + init = sas_tasks.SASInit([0]) +- if solvable: +- goal_fact = (0, 0) ++ goal = sas_tasks.SASGoal([]) ++ util = sas_tasks.SASGoal([]) ++ constant_utility = 0 ++ bound = None ++ if task is not None and task.is_osp_task(): ++ assert solvable, "OSP Tasks should always be solvable!" ++ util_fact = (0, 0, 0) ++ util = sas_tasks.SASUtil([util_fact]) ++ constant_utility = task.constant_utility ++ bound = 1 + else: +- goal_fact = (0, 1) +- goal = sas_tasks.SASGoal([goal_fact]) ++ if solvable: ++ goal_fact = (0, 0) ++ else: ++ goal_fact = (0, 1) ++ goal = sas_tasks.SASGoal([goal_fact]) ++ + operators = [] + axioms = [] + metric = True +- return sas_tasks.SASTask(variables, mutexes, init, goal, ++ return sas_tasks.SASTask(variables, mutexes, init, goal, util, constant_utility, bound, + operators, axioms, metric) + +-def solvable_sas_task(msg): ++ ++def solvable_sas_task(msg, task): + print("%s! Generating solvable task..." % msg) +- return trivial_task(solvable=True) ++ return trivial_task(solvable=True, task=task) + +-def unsolvable_sas_task(msg): ++ ++def unsolvable_sas_task(msg, task): + print("%s! Generating unsolvable task..." % msg) +- return trivial_task(solvable=False) ++ return trivial_task(solvable=False, task=task) ++ + + def pddl_to_sas(task): ++ # if task.is_osp_task() and len(task.utility) > 0: ++ # task.goal = pddl.Conjunction([u[0] for u in task.utility]).simplified() ++ + with timers.timing("Instantiating", block=True): + (relaxed_reachable, atoms, actions, goal_list, axioms, + reachable_action_params) = instantiate.explore(task) + +- if not relaxed_reachable: +- return unsolvable_sas_task("No relaxed solution") +- elif goal_list is None: +- return unsolvable_sas_task("Trivially false goal") ++ if task.is_osp_task(): ++ # task.goal = pddl.Truth() ++ goal_list = [u[0] for u in task.utility] ++ ++ if not task.is_osp_task(): ++ if not relaxed_reachable: ++ return unsolvable_sas_task("No relaxed solution", task) ++ elif goal_list is None: ++ return unsolvable_sas_task("Trivially false goal", task) + + for item in goal_list: + assert isinstance(item, pddl.Literal) +@@ -566,7 +620,7 @@ def pddl_to_sas(task): + sas_task = translate_task( + strips_to_sas, ranges, translation_key, + mutex_dict, mutex_ranges, mutex_key, +- task.init, goal_list, actions, axioms, task.use_min_cost_metric, ++ task.init, goal_list, task.utility, task.bound, actions, axioms, task.use_min_cost_metric, + implied_facts) + + print("%d effect conditions simplified" % +@@ -579,9 +633,9 @@ def pddl_to_sas(task): + try: + simplify.filter_unreachable_propositions(sas_task) + except simplify.Impossible: +- return unsolvable_sas_task("Simplified to trivially false goal") ++ return unsolvable_sas_task("Simplified to trivially false goal", sas_task) + except simplify.TriviallySolvable: +- return solvable_sas_task("Simplified to empty goal") ++ return solvable_sas_task("Simplified to empty goal", sas_task) + + if options.reorder_variables or options.filter_unimportant_vars: + with timers.timing("Reordering and filtering variables", block=True): +@@ -609,26 +663,26 @@ def build_mutex_key(strips_to_sas, groups): + + + def build_implied_facts(strips_to_sas, groups, mutex_groups): +- ## Compute a dictionary mapping facts (FDR pairs) to lists of FDR +- ## pairs implied by that fact. In other words, in all states +- ## containing p, all pairs in implied_facts[p] must also be true. ++ # Compute a dictionary mapping facts (FDR pairs) to lists of FDR ++ # pairs implied by that fact. In other words, in all states ++ # containing p, all pairs in implied_facts[p] must also be true. + ## +- ## There are two simple cases where a pair p implies a pair q != p +- ## in our FDR encodings: +- ## 1. p and q encode the same fact +- ## 2. p encodes a STRIPS proposition X, q encodes a STRIPS literal +- ## "not Y", and X and Y are mutex. ++ # There are two simple cases where a pair p implies a pair q != p ++ # in our FDR encodings: ++ # 1. p and q encode the same fact ++ # 2. p encodes a STRIPS proposition X, q encodes a STRIPS literal ++ # "not Y", and X and Y are mutex. + ## +- ## The first case cannot arise when we use partial encodings, and +- ## when we use full encodings, I don't think it would give us any +- ## additional information to exploit in the operator translation, +- ## so we only use the second case. ++ # The first case cannot arise when we use partial encodings, and ++ # when we use full encodings, I don't think it would give us any ++ # additional information to exploit in the operator translation, ++ # so we only use the second case. + ## +- ## Note that for a pair q to encode a fact "not Y", Y must form a +- ## fact group of size 1. We call such propositions Y "lonely". ++ # Note that for a pair q to encode a fact "not Y", Y must form a ++ # fact group of size 1. We call such propositions Y "lonely". + +- ## In the first step, we compute a dictionary mapping each lonely +- ## proposition to its variable number. ++ # In the first step, we compute a dictionary mapping each lonely ++ # proposition to its variable number. + lonely_propositions = {} + for var_no, group in enumerate(groups): + if len(group) == 1: +@@ -636,10 +690,10 @@ def build_implied_facts(strips_to_sas, groups, mutex_groups): + assert strips_to_sas[lonely_prop] == [(var_no, 0)] + lonely_propositions[lonely_prop] = var_no + +- ## Then we compute implied facts as follows: for each mutex group, +- ## check if prop is lonely (then and only then "not prop" has a +- ## representation as an FDR pair). In that case, all other facts +- ## in this mutex group imply "not prop". ++ # Then we compute implied facts as follows: for each mutex group, ++ # check if prop is lonely (then and only then "not prop" has a ++ # representation as an FDR pair). In that case, all other facts ++ # in this mutex group imply "not prop". + implied_facts = defaultdict(list) + for mutex_group in mutex_groups: + for prop in mutex_group: +@@ -661,6 +715,10 @@ def dump_statistics(sas_task): + if layer >= 0])) + print("Translator facts: %d" % sum(sas_task.variables.ranges)) + print("Translator goal facts: %d" % len(sas_task.goal.pairs)) ++ if sas_task.is_osp_task(): ++ print("Translator utility facts: %d" % len(sas_task.utility.triplets)) ++ print("Translator constant utility: %d" % sas_task.constant_utility) ++ print("Translator plan cost bound: %d" % sas_task.bound) + print("Translator mutex groups: %d" % len(sas_task.mutexes)) + print("Translator total mutex groups size: %d" % + sum(mutex.get_encoding_size() for mutex in sas_task.mutexes)) +@@ -690,7 +748,6 @@ def main(): + for index, effect in reversed(list(enumerate(action.effects))): + if effect.literal.negated: + del action.effects[index] +- + sas_task = pddl_to_sas(task) + dump_statistics(sas_task) + +diff --git a/src/translate/variable_order.py b/src/translate/variable_order.py +index 74c8f5a65..edfe5de1b 100644 +--- a/src/translate/variable_order.py ++++ b/src/translate/variable_order.py +@@ -95,11 +95,13 @@ class CausalGraph: + else: + self.ordering.append(scc[0]) + +- def calculate_important_vars(self, goal): ++ def calculate_important_vars(self, goals_and_utilies): + # Note for future refactoring: it is perhaps more idiomatic + # and efficient to use a set rather than a defaultdict(bool). + necessary = defaultdict(bool) +- for var, _ in goal.pairs: ++ for entry in goals_and_utilies: ++ assert len(entry) == 2 or len(entry) == 3 ++ var = entry[0] + if not necessary[var]: + necessary[var] = True + self.dfs(var, necessary) +@@ -194,6 +196,7 @@ class VariableOrder: + self._apply_to_variables(sas_task.variables) + self._apply_to_init(sas_task.init) + self._apply_to_goal(sas_task.goal) ++ self._apply_to_utility(sas_task.utility) + self._apply_to_mutexes(sas_task.mutexes) + self._apply_to_operators(sas_task.operators) + self._apply_to_axioms(sas_task.axioms) +@@ -220,6 +223,11 @@ class VariableOrder: + for var, val in goal.pairs + if var in self.new_var) + ++ def _apply_to_utility(self, utility): ++ utility.triplets = sorted((self.new_var[var], val, util) ++ for var, val, util in utility.triplets ++ if var in self.new_var) ++ + def _apply_to_mutexes(self, mutexes): + new_mutexes = [] + for group in mutexes: +@@ -277,7 +285,7 @@ def find_and_apply_variable_order(sas_task, reorder_vars=True, + else: + order = list(range(len(sas_task.variables.ranges))) + if filter_unimportant_vars: +- necessary = cg.calculate_important_vars(sas_task.goal) ++ necessary = cg.calculate_important_vars(sas_task.goal.pairs + sas_task.utility.triplets) + print("%s of %s variables necessary." % (len(necessary), + len(order))) + order = [var for var in order if necessary[var]] diff --git a/up_symk/osp_pddl_writer.py b/up_symk/osp_pddl_writer.py new file mode 100644 index 0000000..ca9de0f --- /dev/null +++ b/up_symk/osp_pddl_writer.py @@ -0,0 +1,101 @@ +import unified_planning as up +from unified_planning.io import PDDLWriter + +from typing import IO + + +class OspPDDLWriter(PDDLWriter): + def __init__( + self, + problem: "up.model.Problem", + needs_requirements: bool = True, + rewrite_bool_assignments: bool = False, + ): + self.original_problem = problem + assert len(problem.quality_metrics) <= 2 + + self.osp_qm = next( + ( + qm + for qm in problem.quality_metrics + if isinstance(qm, up.model.metrics.Oversubscription) + ), + None, + ) + + self.plan_cost_qm = next( + ( + qm + for qm in problem.quality_metrics + if isinstance(qm, up.model.metrics.MinimizeActionCosts) + ), + None, + ) + + self.plan_len_qm = next( + ( + qm + for qm in problem.quality_metrics + if isinstance(qm, up.model.metrics.MinimizeSequentialPlanLength) + ), + None, + ) + + assert ( + self.osp_qm + ), "OspPDDLWritter called on a problem that is Oversubscription task!" + + self.goals = list(self.osp_qm.goals.items()) + + new_problem = problem.clone() + new_problem.clear_quality_metrics() + + for qm in self.original_problem.quality_metrics: + if not isinstance(qm, up.model.metrics.Oversubscription): + new_problem.add_quality_metric(qm) + + assert len(new_problem.quality_metrics) <= 1 + + super().__init__( + new_problem, + needs_requirements=needs_requirements, + rewrite_bool_assignments=rewrite_bool_assignments, + ) + + def _write_domain(self, out: IO[str]): + super()._write_domain(out) + out.flush() + + # We replace the goal section with utilities and the dummy plan cost bound + def _write_problem(self, out: IO[str]): + super()._write_problem(out) + out.flush() + + def get_util_pddl(goal): + assert len(goal) == 2 + fact = goal[0] + fact_pddl = f"{fact.fluent().name} {fact.get_nary_expression_string(' ', fact.args)[1:-1]}" + return f"(= ({fact_pddl}) {goal[1]})" + + util_str = "(:utility" + for goal in self.goals: + util_str += " " + get_util_pddl(goal) + util_str += ")\n" + + # Max int - 1 as max plan cost + # Bound is set via the search engine + bound_str = " (:bound 2147483646)\n" + + replace_line_with_string(out.name, ":goal", util_str + bound_str) + + +def replace_line_with_string(file_path: str, target_string: str, new_string: str): + with open(file_path, "r") as file: + lines = file.readlines() + + for i, line in enumerate(lines): + if target_string in line: + lines[i] = new_string + + with open(file_path, "w") as file: + file.writelines(lines) diff --git a/up_symk/symk.py b/up_symk/symk.py index 8d9e990..762a5b1 100644 --- a/up_symk/symk.py +++ b/up_symk/symk.py @@ -1,116 +1,35 @@ -import pkg_resources +from .symk_base import * +from .osp_pddl_writer import * + +import subprocess import sys +import tempfile +import os + +import asyncio +from asyncio.subprocess import PIPE import unified_planning as up -from typing import Callable, Iterator, IO, List, Optional, Tuple, Union +from typing import Callable, IO, List, Optional, Tuple, Union from unified_planning.model import ProblemKind from unified_planning.engines import OptimalityGuarantee from unified_planning.engines import PlanGenerationResultStatus as ResultStatus -from unified_planning.engines import PDDLAnytimePlanner, PDDLPlanner -from unified_planning.engines import OperationMode, Credits -from unified_planning.engines.results import LogLevel, LogMessage, PlanGenerationResult - - -credits = { - "name": "SymK", - "author": "David Speck (cf. https://github.com/speckdavid/symk/blob/master/README.md )", - "contact": "david.speck@liu.se (for UP integration)", - "website": "https://github.com/speckdavid/symk", - "license": "GPLv3", - "short_description": "SymK is a state-of-the-art domain-independent classical optimal and top-k planner.", - "long_description": "SymK is a state-of-the-art domain-independent classical optimal and top-k planner.", -} - - -class SymKMixin(PDDLPlanner): - def __init__( - self, - symk_search_config: Optional[str] = None, - symk_anytime_search_config: Optional[str] = None, - symk_translate_options: Optional[List[str]] = None, - symk_preprocess_options: Optional[List[str]] = None, - symk_search_time_limit: Optional[str] = None, - log_level: str = "info", - ): - super().__init__(rewrite_bool_assignments=True) - self._symk_search_config = symk_search_config - self._symk_anytime_search_config = symk_anytime_search_config - self._symk_translate_options = symk_translate_options - self._symk_preprocess_options = symk_preprocess_options - self._symk_search_time_limit = symk_search_time_limit - self._log_level = log_level - self._guarantee_no_plan_found = ResultStatus.UNSOLVABLE_PROVEN - self._guarantee_metrics_task = ResultStatus.SOLVED_OPTIMALLY - - def _base_cmd(self, plan_filename: str) -> List[str]: - downward = pkg_resources.resource_filename(__name__, "symk/fast-downward.py") - assert sys.executable, "Path to interpreter could not be found" - cmd = [sys.executable, downward, "--plan-file", plan_filename] - if self._symk_search_time_limit is not None: - cmd += ["--search-time-limit", self._symk_search_time_limit] - cmd += ["--log-level", self._log_level] - return cmd - - def _get_cmd( - self, domain_filename: str, problem_filename: str, plan_filename: str - ) -> List[str]: - cmd = self._base_cmd(plan_filename) - cmd += [domain_filename, problem_filename] - if self._symk_translate_options: - cmd += ["--translate-options"] + self._symk_translate_options - if self._symk_preprocess_options: - cmd += ["--preprocess-options"] + self._symk_preprocess_options - if self._symk_search_config: - cmd += ["--search-options", "--search"] + self._symk_search_config.split() - return cmd - - def _get_anytime_cmd( - self, domain_filename: str, problem_filename: str, plan_filename: str - ) -> List[str]: - cmd = self._base_cmd(plan_filename) - cmd += [domain_filename, problem_filename] - if self._symk_translate_options: - cmd += ["--translate-options"] + self._symk_translate_options - if self._symk_preprocess_options: - cmd += ["--preprocess-options"] + self._symk_preprocess_options - if self._symk_anytime_search_config: - cmd += [ - "--search-options", - "--search", - ] + self._symk_anytime_search_config.split() - return cmd - - def _result_status( - self, - problem: "up.model.Problem", - plan: Optional["up.plans.Plan"], - retval: int = None, # Default value for legacy support - log_messages: Optional[List[LogMessage]] = None, - ) -> "up.engines.results.PlanGenerationResultStatus": - def solved(metrics): - if metrics: - return self._guarantee_metrics_task - else: - return ResultStatus.SOLVED_SATISFICING - - # https://www.fast-downward.org/ExitCodes - metrics = problem.quality_metrics - if retval is None: # legacy support - if plan is None: - return self._guarantee_no_plan_found - else: - return solved(metrics) - if retval in (0, 1, 2, 3): - if plan is None: - return self._guarantee_no_plan_found - else: - return solved(metrics) - if retval in (10, 11): - return ResultStatus.UNSOLVABLE_PROVEN - if retval == 12: - return ResultStatus.UNSOLVABLE_INCOMPLETELY - else: - return ResultStatus.INTERNAL_ERROR +from unified_planning.engines import PDDLAnytimePlanner +from unified_planning.engines import Credits +from unified_planning.engines.pddl_planner import * +from unified_planning.engines.results import ( + LogLevel, + PlanGenerationResult, + PlanGenerationResultStatus, +) + +# By default, on non-Windows OSs we use the first method and on Windows we +# always use the second. It is possible to use asyncio under unix by setting +# the environment variable UP_USE_ASYNCIO_PDDL_PLANNER to true. +USE_ASYNCIO_ON_UNIX = False +ENV_USE_ASYNCIO = os.environ.get("UP_USE_ASYNCIO_PDDL_PLANNER") +if ENV_USE_ASYNCIO is not None: + USE_ASYNCIO_ON_UNIX = ENV_USE_ASYNCIO.lower() in ["true", "1"] class SymKOptimalPDDLPlanner(SymKMixin, PDDLAnytimePlanner): @@ -122,20 +41,19 @@ def __init__( symk_preprocess_options: Optional[List[str]] = None, symk_search_time_limit: Optional[str] = None, number_of_plans: Optional[int] = None, + plan_cost_bound: Optional[int] = None, log_level: str = "info", ): PDDLAnytimePlanner.__init__(self) - assert number_of_plans is None or number_of_plans > 0 - if number_of_plans is None: - input_number_of_plans = "infinity" - else: - input_number_of_plans = number_of_plans + + input_number_of_plans = format_input_value(number_of_plans, min_value=1) + input_plan_cost_bound = format_input_value(plan_cost_bound, min_value=0) if symk_search_config is None: - symk_search_config = "sym-bd()" + symk_search_config = f"sym-bd(bound={input_plan_cost_bound})" if symk_anytime_search_config is None: - symk_anytime_search_config = f"symq-bd(plan_selection=top_k(num_plans={input_number_of_plans},dump_plans=true),quality=1.0)" + symk_anytime_search_config = f"symq-bd(plan_selection=top_k(num_plans={input_number_of_plans},dump_plans=true),bound={input_plan_cost_bound},quality=1.0)" SymKMixin.__init__( self, @@ -156,8 +74,7 @@ def get_credits(**kwargs) -> Optional["Credits"]: c = Credits(**credits) details = [ c.long_description, - "The optimal engine uses symbolic bidirectional search by", - "David Speck.", + "The optimal engine uses symbolic search by David Speck.", ] c.long_description = " ".join(details) return c @@ -195,6 +112,7 @@ def supported_kind() -> "ProblemKind": supported_kind.set_quality_metrics("ACTIONS_COST") supported_kind.set_actions_cost_kind("STATIC_FLUENTS_IN_ACTIONS_COST") supported_kind.set_quality_metrics("PLAN_LENGTH") + supported_kind.set_quality_metrics("OVERSUBSCRIPTION") return supported_kind @staticmethod @@ -207,26 +125,168 @@ def ensures(anytime_guarantee: up.engines.AnytimeGuarantee) -> bool: return True return False + def _solve( + self, + problem: "up.model.AbstractProblem", + heuristic: Optional[ + Callable[["up.model.state.ROState"], Optional[float]] + ] = None, + timeout: Optional[float] = None, + output_stream: Optional[Union[Tuple[IO[str], IO[str]], IO[str]]] = None, + anytime: bool = False, + ): + osp_metric = any( + isinstance(qm, up.model.metrics.Oversubscription) + for qm in problem.quality_metrics + ) + + # Call OSP engine + if osp_metric: + assert ( + len(problem.goals) == 0 + ), "The oversubscription engine of Symk does not support hard goals! To simulate hard goals, please assign a very high utility to the hard goals (and set the plan cost bound accordingly)." + + # Replace search engine + self._symk_search_config = replace_search_engine_in_config( + self._symk_search_config, "sym-osp-fw" + ) + if self.ensures(up.engines.AnytimeGuarantee.OPTIMAL_PLANS): + assert "symq" in self._symk_anytime_search_config + self._symk_anytime_search_config = replace_search_engine_in_config( + self._symk_anytime_search_config, "symq-osp-fw" + ) + else: + assert "symk" in self._symk_anytime_search_config + self._symk_anytime_search_config = replace_search_engine_in_config( + self._symk_anytime_search_config, "symk-osp-fw" + ) + + return self._solve_osp_task( + problem, timeout, output_stream, anytime=anytime + ) + + return super()._solve( + problem, heuristic, timeout, output_stream, anytime=anytime + ) + + # SOLVE OSP TASK => We use the PDDL writter and then change the file + # This is definetly not ideal but the best we can do + def _solve_osp_task( + self, + problem: "up.model.AbstractProblem", + timeout: Optional[float] = None, + output_stream: Optional[Union[Tuple[IO[str], IO[str]], IO[str]]] = None, + anytime: bool = False, + ) -> "up.engines.results.PlanGenerationResult": + assert isinstance(problem, up.model.Problem) + self._writer = OspPDDLWriter( + problem, self._needs_requirements, self._rewrite_bool_assignments + ) + plan = None + logs: List["up.engines.results.LogMessage"] = [] + + with tempfile.TemporaryDirectory() as tempdir: + domain_filename = os.path.join(tempdir, "domain.pddl") + problem_filename = os.path.join(tempdir, "problem.pddl") + plan_filename = os.path.join(tempdir, "plan.txt") + self._writer.write_domain(domain_filename) + self._writer.write_problem(problem_filename) + + if anytime: + assert isinstance( + self, up.engines.pddl_anytime_planner.PDDLAnytimePlanner + ) + cmd = self._get_anytime_cmd( + domain_filename, problem_filename, plan_filename + ) + else: + assert self._mode_running == OperationMode.ONESHOT_PLANNER + cmd = self._get_cmd(domain_filename, problem_filename, plan_filename) + + if output_stream is None: + # If we do not have an output stream to write to, we simply call + # a subprocess and retrieve the final output and error with communicate + process = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + timeout_occurred: bool = False + proc_out: List[str] = [] + proc_err: List[str] = [] + try: + out_err_bytes = process.communicate(timeout=timeout) + proc_out, proc_err = [[x.decode()] for x in out_err_bytes] + except subprocess.TimeoutExpired: + timeout_occurred = True + retval = process.returncode + else: + if sys.platform == "win32": + # On windows we have to use asyncio (does not work inside notebooks) + try: + loop = asyncio.ProactorEventLoop() + exec_res = loop.run_until_complete( + run_command_asyncio( + self, cmd, output_stream=output_stream, timeout=timeout + ) + ) + finally: + loop.close() + else: + # On non-windows OSs, we can choose between asyncio and posix + # select (see comment on USE_ASYNCIO_ON_UNIX variable for details) + if USE_ASYNCIO_ON_UNIX: + exec_res = asyncio.run( + run_command_asyncio( + self, cmd, output_stream=output_stream, timeout=timeout + ) + ) + else: + exec_res = run_command_posix_select( + self, cmd, output_stream=output_stream, timeout=timeout + ) + timeout_occurred, (proc_out, proc_err), retval = exec_res + + logs.append(up.engines.results.LogMessage(LogLevel.INFO, "".join(proc_out))) + logs.append( + up.engines.results.LogMessage(LogLevel.ERROR, "".join(proc_err)) + ) + if os.path.isfile(plan_filename): + plan = self._plan_from_file( + problem, plan_filename, self._writer.get_item_named + ) + if timeout_occurred and retval != 0: + return PlanGenerationResult( + PlanGenerationResultStatus.TIMEOUT, + plan=plan, + log_messages=logs, + engine_name=self.name, + ) + status: PlanGenerationResultStatus = self._result_status( + problem, plan, retval, logs + ) + res = PlanGenerationResult( + status, plan, log_messages=logs, engine_name=self.name + ) + return res + class SymKPDDLPlanner(SymKOptimalPDDLPlanner): def __init__( self, symk_anytime_search_config: Optional[str] = None, number_of_plans: Optional[int] = None, + plan_cost_bound: Optional[int] = None, log_level: str = "info", ): - assert number_of_plans is None or number_of_plans > 0 - if number_of_plans is None: - input_number_of_plans = "infinity" - else: - input_number_of_plans = number_of_plans + input_number_of_plans = format_input_value(number_of_plans, min_value=1) + input_plan_cost_bound = format_input_value(plan_cost_bound, min_value=0) if symk_anytime_search_config is None: - symk_anytime_search_config = f"symk-bd(plan_selection=top_k(num_plans={input_number_of_plans},dump_plans=true))" + symk_anytime_search_config = f"symk-bd(plan_selection=top_k(num_plans={input_number_of_plans},dump_plans=true),bound={input_plan_cost_bound})" super().__init__( symk_anytime_search_config=symk_anytime_search_config, number_of_plans=number_of_plans, + plan_cost_bound=plan_cost_bound, log_level=log_level, ) @@ -234,6 +294,16 @@ def __init__( def name(self) -> str: return "SymK" + # Oneshot planner is optimal + @staticmethod + def satisfies(optimality_guarantee: "OptimalityGuarantee") -> bool: + return True + + # Plans are reported with increasing costs thus potentially also non-optimal ones + @staticmethod + def ensures(anytime_guarantee: up.engines.AnytimeGuarantee) -> bool: + return False + def _solve( self, problem: "up.model.AbstractProblem", @@ -248,16 +318,7 @@ def _solve( self._guarantee_metrics_task = ResultStatus.SOLVED_SATISFICING else: self._guarantee_metrics_task = ResultStatus.SOLVED_OPTIMALLY + return super()._solve( problem, heuristic, timeout, output_stream, anytime=anytime ) - - # Oneshot planner is optimal - @staticmethod - def satisfies(optimality_guarantee: "OptimalityGuarantee") -> bool: - return True - - # Plans are reported with increasing costs thus potentially also non-optimal ones - @staticmethod - def ensures(anytime_guarantee: up.engines.AnytimeGuarantee) -> bool: - return False diff --git a/up_symk/symk_base.py b/up_symk/symk_base.py new file mode 100644 index 0000000..47f2869 --- /dev/null +++ b/up_symk/symk_base.py @@ -0,0 +1,135 @@ +import pkg_resources +import sys + +import unified_planning as up + +from fractions import Fraction +from typing import List, Optional, Union +from unified_planning.engines import PlanGenerationResultStatus as ResultStatus +from unified_planning.engines import PDDLPlanner +from unified_planning.engines.results import LogMessage +from unified_planning.utils import powerset + + +credits = { + "name": "SymK", + "author": "David Speck (cf. https://github.com/speckdavid/symk/blob/master/README.md )", + "contact": "david.speck@liu.se (for UP integration)", + "website": "https://github.com/speckdavid/symk", + "license": "GPLv3", + "short_description": "SymK is a state-of-the-art domain-independent optimal and top-k planner.", + "long_description": "SymK is a state-of-the-art domain-independent optimal and top-k planner.", +} + + +class SymKMixin(PDDLPlanner): + def __init__( + self, + symk_search_config: Optional[str] = None, + symk_anytime_search_config: Optional[str] = None, + symk_translate_options: Optional[List[str]] = None, + symk_preprocess_options: Optional[List[str]] = None, + symk_search_time_limit: Optional[str] = None, + log_level: str = "info", + ): + super().__init__(rewrite_bool_assignments=True) + self._symk_search_config = symk_search_config + self._symk_anytime_search_config = symk_anytime_search_config + self._symk_translate_options = symk_translate_options + self._symk_preprocess_options = symk_preprocess_options + self._symk_search_time_limit = symk_search_time_limit + self._log_level = log_level + self._guarantee_no_plan_found = ResultStatus.UNSOLVABLE_PROVEN + self._guarantee_metrics_task = ResultStatus.SOLVED_OPTIMALLY + + def _base_cmd(self, plan_filename: str) -> List[str]: + downward = pkg_resources.resource_filename(__name__, "symk/fast-downward.py") + assert sys.executable, "Path to interpreter could not be found" + cmd = [sys.executable, downward, "--plan-file", plan_filename] + if self._symk_search_time_limit is not None: + cmd += ["--search-time-limit", self._symk_search_time_limit] + cmd += ["--log-level", self._log_level] + return cmd + + def _get_cmd( + self, domain_filename: str, problem_filename: str, plan_filename: str + ) -> List[str]: + cmd = self._base_cmd(plan_filename) + cmd += [domain_filename, problem_filename] + if self._symk_translate_options: + cmd += ["--translate-options"] + self._symk_translate_options + if self._symk_preprocess_options: + cmd += ["--preprocess-options"] + self._symk_preprocess_options + if self._symk_search_config: + cmd += ["--search-options", "--search"] + self._symk_search_config.split() + return cmd + + def _get_anytime_cmd( + self, domain_filename: str, problem_filename: str, plan_filename: str + ) -> List[str]: + cmd = self._base_cmd(plan_filename) + cmd += [domain_filename, problem_filename] + if self._symk_translate_options: + cmd += ["--translate-options"] + self._symk_translate_options + if self._symk_preprocess_options: + cmd += ["--preprocess-options"] + self._symk_preprocess_options + if self._symk_anytime_search_config: + cmd += [ + "--search-options", + "--search", + ] + self._symk_anytime_search_config.split() + return cmd + + def _result_status( + self, + problem: "up.model.Problem", + plan: Optional["up.plans.Plan"], + retval: int = None, # Default value for legacy support + log_messages: Optional[List[LogMessage]] = None, + ) -> "up.engines.results.PlanGenerationResultStatus": + def solved(metrics): + if metrics: + return self._guarantee_metrics_task + else: + return ResultStatus.SOLVED_SATISFICING + + # https://www.fast-downward.org/ExitCodes + metrics = problem.quality_metrics + if retval is None: # legacy support + if plan is None: + return self._guarantee_no_plan_found + else: + return solved(metrics) + if retval in (0, 1, 2, 3): + if plan is None: + return self._guarantee_no_plan_found + else: + return solved(metrics) + if retval in (10, 11): + return ResultStatus.UNSOLVABLE_PROVEN + if retval == 12: + return ResultStatus.UNSOLVABLE_INCOMPLETELY + else: + return ResultStatus.INTERNAL_ERROR + + +def replace_search_engine_in_config(search_config: str, new_engine: str) -> str: + # Find the index of the first "(" in the search engine configuration + search_config_last_id = search_config.find("(") + + # Make sure that "(" is found in the search engine configuration + assert ( + search_config_last_id != -1 + ), "Opening parenthesis not found in search engine configuration." + + # Replace the search engine substring with the new engine + updated_config = f"{new_engine}{search_config[search_config_last_id:]}" + + return updated_config + + +def format_input_value(value, min_value=0): + if value is None: + return "infinity" + assert value >= min_value if value is not None else True + return value