aiplan4eu / unified-planning

The AIPlan4EU Unified Planning Library
Apache License 2.0
181 stars 39 forks source link

Using IntType task parameters #565

Closed MathisFederico closed 7 months ago

MathisFederico commented 7 months ago

User Story

As a user discovering HTN I want to use IntType parameters in hierarchical tasks to solve my problem/domain.

When using aries, the only available hierarchical planner in the unified planning framework, it raises a typerror.

Acceptance Criteria

Additional Material

 problem name = HierarchyCraft

 types = [zone, player_item, zone_item]

 fluents = [
   bool pos[zone=zone]
   bool visited[zone=zone]
   integer amount[item=player_item]
   integer amount_at[item=zone_item, zone=zone]
 ]

 actions = [
   action 0_move_to_other_zone(zone loc) {
     preconditions = [
       pos(loc)
       pos(start)
     ]
     effects = [
       pos(loc) := false
       visited(other_zone) := true
       pos(other_zone) := true
     ]
   }
   action 1_search_wood {
     preconditions = [
     ]
     effects = [
       amount(wood) += 1
     ]
   }
   action 2_craft_plank {
     preconditions = [
       (1 <= amount(wood))
     ]
     effects = [
       amount(plank) += 4
       amount(wood) -= 1
     ]
   }
   action 3_craft_table(zone loc) {
     preconditions = [
       pos(loc)
       (4 <= amount(plank))
     ]
     effects = [
       amount(plank) -= 4
       amount_at(table_in_zone, loc) += 1
     ]
   }
 ]

 objects = [
   zone: [start, other_zone]
   player_item: [wood, plank]
   zone_item: [table_in_zone]
 ]

 initial fluents default = [
   bool pos[zone=zone] := false
   bool visited[zone=zone] := false
   integer amount[item=player_item] := 0
   integer amount_at[item=zone_item, zone=zone] := 0
 ]

 initial values = [
   pos(start) := true
   visited(start) := true
   pos(other_zone) := false
   visited(other_zone) := false
   amount(wood) := 0
   amount(plank) := 0
 ]

 goals = [
 ]

 abstract tasks = [
   get-enough-of-wood[quantity=integer]
   get-enough-of-plank[quantity=integer]
 ]

 methods = [
   method has-enough-of-wood(integer quantity) {
     task = get-enough-of-wood(integer quantity)
     preconditions = [
       (quantity <= amount(wood))
     ]
   }
   method execute-search_wood(integer quantity) {
     task = get-enough-of-wood(integer quantity)
     subtasks = [
         _t3: get-enough-of-wood((quantity - 1))
         _t4: 1_search_wood()
     ]
   }
   method has-enough-of-plank(integer quantity) {
     task = get-enough-of-plank(integer quantity)
     preconditions = [
       (quantity <= amount(plank))
     ]
   }
   method execute-craft_plank(integer quantity) {
     task = get-enough-of-plank(integer quantity)
     preconditions = [
       (1 <= amount(wood))
     ]
     subtasks = [
         _t5: get-enough-of-plank((quantity - 4))
         _t6: 3_craft_plank()
     ]
   }
 ]

 task network {
   subtasks = [
     Get 3 wood: get-enough-of-wood(3)
   ]
 }

Attention Points

arbimo commented 7 months ago

To be able to reproduce it, could you post the code generating the model ?

MathisFederico commented 7 months ago

Here is an even smaller example with code:

from typing import Dict
import unified_planning.shortcuts as ups
from unified_planning.model.htn import HierarchicalProblem, Method, Task
from unified_planning.engines.results import PlanGenerationResult

PLAYER_ITEM_TYPE = ups.UserType("player_item")

def generate_hproblem() -> HierarchicalProblem:
    hproblem = HierarchicalProblem("Small Hierarchical example")

    # Types and objects
    items_obj: Dict[str, "ups.Object"] = {
        "wood": ups.Object("wood", PLAYER_ITEM_TYPE),
        "plank": ups.Object("plank", PLAYER_ITEM_TYPE),
    }

    hproblem.add_objects(items_obj.values())

    # Numeric fluents
    amount = ups.Fluent("amount", ups.IntType(), item=PLAYER_ITEM_TYPE)
    hproblem.add_fluent(amount, default_initial_value=0)

    # Actions
    actions = []

    ## action 1_search_wood
    action_1 = ups.InstantaneousAction("1_search_wood")
    action_1.add_increase_effect(amount(items_obj["wood"]), 1)
    actions.append(action_1)

    ## action 2_craft_plank
    action_2 = ups.InstantaneousAction("2_craft_plank")
    action_2.add_precondition(ups.GE(amount(items_obj["wood"]), 1))
    action_2.add_decrease_effect(amount(items_obj["wood"]), 1)
    action_2.add_increase_effect(amount(items_obj["plank"]), 4)
    actions.append(action_2)

    hproblem.add_actions(actions)

    # Tasks and methods
    get_enough_of_item_task: Dict["str", "Task"] = {}

    ## get_enough_wood
    task = hproblem.add_task("get-enough-of-wood", quantity=ups.IntType())
    get_enough_of_item_task["wood"] = task

    ### noop method
    noop_method = Method("has-enough-of-wood", quantity=ups.IntType())
    noop_method.add_precondition(
        ups.GE(amount(items_obj["wood"]), noop_method.quantity)
    )
    noop_method.set_task(task)
    hproblem.add_method(noop_method)

    ### with action 1_search_wood
    method_execute_1 = Method("execute-search-wood", quantity=ups.IntType())
    quantity = method_execute_1.parameter("quantity")
    stack_quantity = 1

    get_nearly_enough_wood = method_execute_1.add_subtask(
        get_enough_of_item_task["wood"],
        quantity - stack_quantity,
    )
    execute_search_wood = method_execute_1.add_subtask(action_1)
    method_execute_1.set_ordered([get_nearly_enough_wood, execute_search_wood])
    method_execute_1.set_task(task, quantity)
    hproblem.add_method(method_execute_1)

    ## get_enough_planks
    task = hproblem.add_task("get-enough-of-plank", quantity=ups.IntType())
    get_enough_of_item_task["plank"] = task

    ### noop method
    noop_method = Method("has-enough-of-plank", quantity=ups.IntType())
    quantity = noop_method.parameter("quantity")
    noop_method.add_precondition(ups.GE(amount(items_obj["plank"]), quantity))
    noop_method.set_task(task)
    hproblem.add_method(noop_method)

    ### with action 2_craft_plank
    method_execute_2 = Method("execute-search-plank", quantity=ups.IntType())
    quantity = method_execute_2.parameter("quantity")
    stack_quantity = 4

    get_nearly_enough_plank = method_execute_2.add_subtask(
        get_enough_of_item_task["plank"],
        quantity - stack_quantity,
    )
    execute_search_plank = method_execute_2.add_subtask(action_2)
    method_execute_2.set_ordered([get_nearly_enough_plank, execute_search_plank])
    method_execute_2.set_task(task, quantity)
    hproblem.add_method(method_execute_2)

    hproblem.task_network.add_subtask(
        get_enough_of_item_task["plank"], 5, ident="Get 5 planks"
    )

    return hproblem

def main():
    hproblem = generate_hproblem()
    print(hproblem, end="\n\n\n")
    with ups.OneshotPlanner(name="aries") as planner:
        results: "PlanGenerationResult" = planner.solve(hproblem)
    if results.plan is None:
        raise ValueError(
            "Not plan could be found for this problem.\n"
            f"Planner status: {results.status}\n"
            f"Logs: {[log.message for log in results.log_messages]}"
        )
    print(f"Plan found: {results.plan}")

if __name__ == "__main__":
    main()

Will generate output:

problem name = Small Hierarchical example

types = [player_item]

fluents = [
  integer amount[item=player_item]
]

actions = [
  action 1_search_wood {
    preconditions = [
    ]
    effects = [
      amount(wood) += 1
    ]
  }
  action 2_craft_plank {
    preconditions = [
      (1 <= amount(wood))
    ]
    effects = [
      amount(wood) -= 1
      amount(plank) += 4
    ]
  }
]

objects = [
  player_item: [wood, plank]
]

initial fluents default = [
  integer amount[item=player_item] := 0
]

initial values = [
]

goals = [
]

abstract tasks = [
  get-enough-of-wood[quantity=integer]
  get-enough-of-plank[quantity=integer]
]

methods = [
  method has-enough-of-wood(integer quantity) {
    task = get-enough-of-wood(integer quantity)
    preconditions = [
      (quantity <= amount(wood))
    ]
  }
  method execute-search-wood(integer quantity) {
    task = get-enough-of-wood(integer quantity)
    subtasks = [
        _t1: get-enough-of-wood((quantity - 1))
        _t2: 1_search_wood()
    ]
  }
  method has-enough-of-plank(integer quantity) {
    task = get-enough-of-plank(integer quantity)
    preconditions = [
      (quantity <= amount(plank))
    ]
  }
  method execute-search-plank(integer quantity) {
    task = get-enough-of-plank(integer quantity)
    subtasks = [
        _t3: get-enough-of-plank((quantity - 4))
        _t4: 2_craft_plank()
    ]
  }
]

task network {
  subtasks = [
    Get 5 planks: get-enough-of-plank(5)
  ]
}

Traceback (most recent call last):
  File "d:\Data\Projects\Github\Recherche\HierarchyCraft\small_hproblem.py", line 117, in <module>
    main()
  File "d:\Data\Projects\Github\Recherche\HierarchyCraft\small_hproblem.py", line 108, in main 
    raise ValueError(
ValueError: Not plan could be found for this problem.
Planner status: PlanGenerationResultStatus.INTERNAL_ERROR
Logs: ['type error\n    Context: In problem Small Hierarchical example_domain/Small Hierarchical example']
arbimo commented 7 months ago

Indeed, the Aries engine supports numeric parameters but our conversion from UP conservatively rejected it. It should work in the latest development version (see https://github.com/plaans/aries/tree/master/planning/unified/plugin )

By the way, it seems that your model has a few quirks :

Below is the model with these flaws fixed. I will close this issue as it is not actually related to unified-planning. For any follow up bugs on Aries or its integration, you can open them in the aries repository directly.

def generate_hproblem() -> HierarchicalProblem:
    hproblem = HierarchicalProblem("Small Hierarchical example")

    # Types and objects
    items_obj: Dict[str, "ups.Object"] = {
        "wood": ups.Object("wood", PLAYER_ITEM_TYPE),
        "plank": ups.Object("plank", PLAYER_ITEM_TYPE),
    }

    hproblem.add_objects(items_obj.values())

    # Numeric fluents
    amount = ups.Fluent("amount", ups.IntType(), item=PLAYER_ITEM_TYPE)
    hproblem.add_fluent(amount, default_initial_value=0)

    # Actions
    actions = []

    ## action 1_search_wood
    action_1 = ups.InstantaneousAction("1_search_wood")
    action_1.add_increase_effect(amount(items_obj["wood"]), 1)
    actions.append(action_1)

    ## action 2_craft_plank
    action_2 = ups.InstantaneousAction("2_craft_plank")
    action_2.add_precondition(ups.GE(amount(items_obj["wood"]), 1))
    action_2.add_decrease_effect(amount(items_obj["wood"]), 1)
    action_2.add_increase_effect(amount(items_obj["plank"]), 4)
    actions.append(action_2)

    hproblem.add_actions(actions)

    # Tasks and methods
    get_enough_of_item_task: Dict["str", "Task"] = {}

    ## get_enough_wood
    task = hproblem.add_task("get-enough-of-wood", quantity=ups.IntType())
    get_enough_of_item_task["wood"] = task

    ### noop method
    noop_method = Method("has-enough-of-wood", quantity=ups.IntType())
    noop_method.add_precondition(
        ups.GE(amount(items_obj["wood"]), noop_method.quantity)
    )
    noop_method.set_task(task)
    hproblem.add_method(noop_method)

    ### with action 1_search_wood
    method_execute_1 = Method("execute-search-wood", quantity=ups.IntType())
    quantity = method_execute_1.parameter("quantity")
    stack_quantity = 1

    get_nearly_enough_wood = method_execute_1.add_subtask(
        get_enough_of_item_task["wood"],
        quantity - stack_quantity,
    )
    execute_search_wood = method_execute_1.add_subtask(action_1)
    method_execute_1.set_ordered(get_nearly_enough_wood, execute_search_wood)  # enforce order
    method_execute_1.set_task(task, quantity)
    hproblem.add_method(method_execute_1)

    ## get_enough_planks
    task = hproblem.add_task("get-enough-of-plank", quantity=ups.IntType())
    get_enough_of_item_task["plank"] = task

    ### noop method
    noop_method = Method("has-enough-of-plank", quantity=ups.IntType())
    quantity = noop_method.parameter("quantity")
    noop_method.add_precondition(ups.GE(amount(items_obj["plank"]), quantity))
    noop_method.set_task(task)
    hproblem.add_method(noop_method)

    ### with action 2_craft_plank
    method_execute_2 = Method("execute-search-plank", quantity=ups.IntType())
    quantity = method_execute_2.parameter("quantity")
    stack_quantity = 4
    # do not overproduce
    method_execute_2.add_precondition(ups.LT(amount(items_obj["plank"]), quantity))

    get_nearly_enough_plank = method_execute_2.add_subtask(
        get_enough_of_item_task["plank"],
        quantity - stack_quantity,
    )
    get_nearly_enough_wood = method_execute_2.add_subtask(  # allow collecting wood
        get_enough_of_item_task["wood"],
        1,
    )
    execute_search_plank = method_execute_2.add_subtask(action_2) 
    method_execute_2.set_ordered(get_nearly_enough_plank, execute_search_plank) # enforce order
    method_execute_2.set_task(task, quantity)
    hproblem.add_method(method_execute_2)

    hproblem.task_network.add_subtask(
        get_enough_of_item_task["plank"], 13, ident="Get 5 planks"
    )

    return hproblem
MathisFederico commented 7 months ago

Ok thanks a lot !