aiplan4eu / unified-planning

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

Add ability to validate that the preconditions of future actions still hold for a given current state #607

Open scastro-bdai opened 3 months ago

scastro-bdai commented 3 months ago

User Story

As a user who wants to track the execution of plans to see if any preconditions have been violated, I want the ability to easily check the (grounded) preconditions and effects of my actual action instances.

Basically, the workflow would be to iteratively

Acceptance Criteria

Additional Material

I am not sure if I'm duplicating functionality because I can't read the code/documentation correctly, but this worked for me for the first criterion:

I was able to hack around this with the following code:

# Do the planning
with OneshotPlanner(name="tamer", problem_kind=problem.kind) as planner:
    result = planner.solve(problem)

# Try to parse out the preconditions and effects of an action
for action_instance in result.plan.actions:
    subs = {
        param: val for param, val in 
        zip(action_instance.action.parameters, action_instance.actual_parameters)
    }
    preconditions = [p.substitute(subs) for p in action_instance.action.preconditions]
    effects = [(e.fluent.substitute(subs), e.value) for e in action_instance.action.effects]
    print(f"Action instance: {action_instance}")
    print(f"  Preconditions: {preconditions}")
    print(f"  Effects: {effects}")

This code would yield something like this:

SequentialPlan:
    pick(robby, apple, kitchen)
    move(robby, kitchen, bedroom)
    place(robby, apple, bedroom)

Action instance: pick(robby, apple, kitchen)
  Preconditions: [((robot_at(robby, kitchen) and hand_empty(robby)) and at(apple, kitchen))]
  Effects: [(at(apple, kitchen), false), (hand_empty(robby), false), (holding(robby, apple), true), (can_move(robby), true)]

Action instance: move(robby, kitchen, bedroom)
  Preconditions: [robot_at(robby, kitchen), can_move(robby)]
  Effects: [(robot_at(robby, kitchen), false), (robot_at(robby, bedroom), true), (can_move(robby), false)]

Action instance: place(robby, apple, bedroom)
  Preconditions: [robot_at(robby, bedroom), holding(robby, apple)]
  Effects: [(at(apple, bedroom), true), (hand_empty(robby), true), (holding(robby, apple), false), (can_move(robby), true)]

I guess there are "Grounders" available in this library that appear to do the above, but they seem very heavyweight for what I'm trying to achieve.

Then, you could start with a specific state and apply the effects by doing:

new_state = problem.initial_values
new_state[some_effect] = True # or False

Finally, you could validate the preconditions by checking whether the new state includes them.

if new_state[some_precondition]:
    # Do something
    # Of course, need to also check for logical formulas on said preconditions

Am I on the right track? Would this be useful?

Attention Points

N/A


Framba-Luca commented 3 months ago

Hi @scastro-bdai , check out the SequentialSimulator operation mode (documented here with also a notebook here).

It should fit your use-case as long as you work with SequentialPlans!

scastro-bdai commented 3 months ago

Thanks @Framba-Luca -- indeed that solves my use case!

with SequentialSimulator(problem) as sim:
    state = sim.get_initial_state()
    print(f"Initial sim state: {state}")

    act = result.plan.actions[0]
    res = sim.is_applicable(state, act)
    print(f"Is action {act} applicable? {res}")

    state._values[robot_at(robby, kitchen)] = Bool(False)
    res = sim.is_applicable(state, act)
    print(f"Is action {act} applicable? {res}")

yields

Initial sim state: {can_move(robby): true, hand_empty(robby): true, holding(robby, apple): false, holding(robby, banana): false, robot_at(robby, kitchen): true, robot_at(robby, bedroom): false, at(apple, kitchen): true, at(apple, bedroom): false, at(banana, kitchen): false, at(banana, bedroom): true}

Is action pick(robby, apple, kitchen) applicable? True

Is action pick(robby, apple, kitchen) applicable? False

I can also do:

sim.get_unsatisfied_conditions(state, act)

to get

([robot_at(robby, kitchen)], <InapplicabilityReasons.VIOLATES_CONDITIONS: 1>)

Maybe my one piece of feedback is to add a set_value() method to State so the line to manually mutate the state value doesn't index a private attribute _values.

Framba-Luca commented 3 months ago

@scastro-bdai Actually the State should be immutable (eq and hash methods require immutability, otherwise bugs might arise; the internal implementations assumes immutability).

So, instead of changing a value, we designed the make_child method, that creates a new state with the updated values.

I am copy-pasting here the method because the UPState is not in the api documentation.

    def make_child(
        self,
        updated_values: Dict["up.model.FNode", "up.model.FNode"],
    ) -> "UPState":
        """
        Returns a different `UPState` in which every value in updated_values.keys() is evaluated as his mapping
        in new the `updated_values` dict and every other value is evaluated as in `self`.

        :param updated_values: The dictionary that contains the `values` that need to be updated in the new `UPState`.
        :return: The new `UPState` created.
        """
scastro-bdai commented 3 months ago

Worked perfectly. Thanks for all your help!