Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inefficient existential preconditions resolution #328

Open
Rezenders opened this issue Oct 4, 2024 · 9 comments
Open

Inefficient existential preconditions resolution #328

Rezenders opened this issue Oct 4, 2024 · 9 comments

Comments

@Rezenders
Copy link
Contributor

Hi @fmrico,

Unfortunately, the solution I proposed for existential predicates is inefficient. It is literally exploring the whole state space to check if the predicates are satisfied. This can easily explode. For example, if there are 75 instances and an existential precondition has 5 variables, this results in 75^5 possible combinations to check, which is unfeasible.

A concrete example (complete formulation can be found here):

	(:derived (inferred-Fg_status ?fg ?IN_ERROR_NFR_string)
		(and
			(= ?IN_ERROR_NFR_string IN_ERROR_NFR_string)
			(exists (?mqa ?eqa ?mqav ?fd ?eqav)
 				(and
					(inferred-FunctionGrounding ?fg)
					(inferred-HasQAvalue ?fg ?mqa)
					(inferred-IsQAtype ?eqa water_visibility)
					(inferred-HasValue ?mqa ?mqav)
					(inferred-TypeFD ?fg ?fd)
					(inferred-HasValue ?eqa ?eqav)
					(inferred-HasQAestimation ?fd ?eqa)
					(inferred-IsQAtype ?mqa water_visibility)
					(inferred-FunctionDesign ?fd)
					(inferred-QAvalue ?mqa)
					(inferred-QAvalue ?eqa)
					(inferred-LessThan ?mqav ?eqav)
				)
			)
 		)
 	)

We need to come up with a mechanism to prune the possible variable combinations. I don't have any good ideas so far.

@tobiaswjohn
Copy link

The general procedure should be to start from the state, i.e. known facts and build the set of all derived facts from that. This applies to the existential quantification and the derived predicates.

I assume, that there is a set S that contains all atomic formulas of the form (p a_1 ... a_n) that are true in the current state

  • notations:
    • variables are named ?x_1 until ?x_n
    • object names are a_1 until a_n
    • derived predicates have the form (:derived (p ?x_1 ... ?x_n) P'), where the predicate (p ?x_1 ... ?x_n) is derived, if the condition P' is satisfied (where ?x_1,...,?x_n occur freely in P')

Algorithm for derived predicates

  • The general semantics of derived predicates is defined in this paper (see page 525 and the algorithm on page 526). The algorithm here is a version of that. In general, the algorithm tries to iteratively apply all rules (function derive) until no additional new facts can be derived.

  • algorithm to compute all derived predicates from a state S, i.e. a set of satisfied atomic formulas

allDerived(S):
   S' := union(S, derive(S))
   while (S != S'):
      S := S'
      S' := union(S, derive(S))
   return S
  • one single inference step for the derived predicates
derive(S):
   D := Set()
   forAll derived predicates derPred:
         case derPred = (:derived (p ?x_1 ... ?x_n) P') :
            E := eval(P', S)
            Derived := {(p a_1 ... a_n) | (x_1=a_1,...,x_n=a_n) in E}
            // CAVE: if some variables are never used in P': add all combinations of them to Derived
            D := union(D, Derived)
   return D
  • the function eval returns for a piece of PDDL code (e.g. predicate, existential formula) the possible assignments that hold (given a state)
  • variables that do not occur in the assignment are not restricted by the formula → can be assigned any object
  • i.e. it maps PDDL code Pand state Sto a set
{{?x_1=a_1,...,?x_n=a_n}, {?x_1=a_1',...,?x_n=a_n'},...}
eval(P, S):
   case P = (p ?x_1 ... ?x_n) :
      return {{?x_1=a_1,...,?x_n=a_n} | (p a_1 ... a_n) in S}

   case P = (exists (?x_j) P') :
      E := eval(P', S)
      return {{?x_1=a_1,...,?x_n=a_n} | {?x_1=a_1,...,?x_n=a_n, ?x_j=a_j} in E}

   case P = (and (P' R')) :
      E_1 := eval(P', S)
      E_2 := eval(R', S)
      return join(E_1, E_2)   // join via shared variables

Algorithm to decide preconditions

Based on the before mentioned, we can decide, if the precondition of an action is satisfied.

  • general algorithm to compute, if preconditions are satisfied: compute all derived predicates → evaluate preconditions based on derived predicates
  • algorithm: returns true if precondition is satisfied
  • assumption: precondition is something that can be used in evalfunction
      evalPrecond(pre):
       S := state
       S_D := allDerived(S)
       if {?x_1=a_1,...,?x_n=a_n} in eval(pre, S_D): // CAVE: if eval-function does not set all variables --> those can have any value
          return true
       else:
          return false
    
  • note: one could make this more efficient by feeding the already assigned variables to the eval function, but this would make the pseudocode more ugly

@Rezenders
Copy link
Contributor Author

@tobiaswjohn Thanks! I am working on improving the algorithm with your input. I will come back with some questions soon :)

@tobiaswjohn
Copy link

@Rezenders I see you did quite some work and made good progress on this issue. You made quite some comments in the pull request #330
Are there any specific points, where my input would be helpful?

Also: does the evaluation really take more than 30s on the (rather simple) example? This still sounds like quite some time :/

@Rezenders
Copy link
Contributor Author

Also: does the evaluation really take more than 30s on the (rather simple) example? This still sounds like quite some time

Yes, it takes around 37s to solve all derived predicates in the domain_derived.pddl. However, I don't think this is a problem necessarily related to the new evaluation implementation, using only the SymK planner to find a plan using the previous domain and the problem_derived.pddl problem formulation, takes around 278 seconds (ps: you need to remove the fluents from the pddl formulation to make it work with symk).

ros2 run symk_ros fast-downward.py domain_derived.pddl problem_derived.pddl --search "sym_bd()"
INFO     planner time limit: None
INFO     planner memory limit: None

INFO     Running translator.
INFO     translator stdin: None
INFO     translator time limit: None
INFO     translator memory limit: None
INFO     translator command line string: /usr/bin/python3 /home/gus/symk_ros_ws/install/symk_ros/share/symk_ros/builds/release/bin/translate/translate.py domain_derived.pddl problem_derived.pddl --sas-file output.sas
Parsing...
Parsing: [0.000s CPU, 0.002s wall-clock]
Normalizing task... [0.000s CPU, 0.000s wall-clock]
Instantiating...
Generating Datalog program... [0.000s CPU, 0.000s wall-clock]
Normalizing Datalog program...
Normalizing Datalog program: [0.000s CPU, 0.002s wall-clock]
Preparing model... [0.000s CPU, 0.001s wall-clock]
Generated 67 rules.
Computing model... [59.000s CPU, 59.011s wall-clock]
3221937 relevant atoms
9022900 auxiliary atoms
12244837 final queue length
12889169 total queue pushes
Completing instantiation... [42.910s CPU, 42.940s wall-clock]
Instantiating: [104.750s CPU, 104.793s wall-clock]
Computing fact groups...
Finding invariants...
9 initial candidates
Finding invariants: [0.830s CPU, 0.835s wall-clock]
Checking invariant weight... [0.000s CPU, 0.000s wall-clock]
Instantiating groups... [0.000s CPU, 0.000s wall-clock]
Collecting mutex groups... [0.610s CPU, 0.594s wall-clock]
Choosing groups...
1288718 uncovered facts
Choosing groups: [8.050s CPU, 8.059s wall-clock]
Building translation key... [3.240s CPU, 3.241s wall-clock]
Computing fact groups: [26.610s CPU, 26.611s wall-clock]
Building STRIPS to SAS dictionary... [1.450s CPU, 1.457s wall-clock]
Building dictionary for full mutex groups... [9.400s CPU, 9.400s wall-clock]
Building mutex information...
Building mutex information: [1.280s CPU, 1.281s wall-clock]
Translating task...
Processing axioms...
Simplifying axioms... [6.330s CPU, 6.330s wall-clock]
Translator axioms removed by simplifying: 0
Processing axioms: [46.670s CPU, 46.680s wall-clock]
Translating task: [89.590s CPU, 89.637s wall-clock]
0 effect conditions simplified
0 implied preconditions added
Detecting unreachable propositions...
0 operators removed
0 axioms removed
74 propositions removed
Detecting unreachable propositions: [31.440s CPU, 31.461s wall-clock]
Reordering and filtering variables...
9 of 1288681 variables necessary.
0 of 0 mutex groups necessary.
1 of 644332 operators necessary.
8 of 644349 axiom rules necessary.
Reordering and filtering variables: [8.880s CPU, 8.895s wall-clock]
Translator variables: 9
Translator derived variables: 8
Translator facts: 18
Translator goal facts: 1
Translator mutex groups: 0
Translator total mutex groups size: 0
Translator operators: 1
Translator axioms: 8
Translator task size: 46
Translator peak memory: 7342432 KB
Writing output... [0.000s CPU, 0.001s wall-clock]
Done! [277.690s CPU, 277.894s wall-clock]
translate exit code: 0

INFO     Running preprocessor (release).
INFO     preprocess stdin: output.sas
INFO     preprocess time limit: None
INFO     preprocess memory limit: None
INFO     preprocess command line string: /home/gus/symk_ros_ws/install/symk_ros/share/symk_ros/builds/release/bin/preprocess < output.sas
Building causal graph...
The causal graph is acyclic.
9 variables of 9 necessary
0 of 0 mutex groups necessary.
1 of 1 operators necessary.
8 of 8 axiom rules necessary.
Disabling h2 analysis because it does not currently support axioms
Preprocessor variables: 9
Preprocessor facts: 18
Preprocessor derived variables: 8
Preprocessor operators: 1
Preprocessor mutex groups: 0
Preprocessor task size: 46
Writing output...
done
preprocess exit code: 0

INFO     Running search (release).
INFO     search stdin: output.sas
INFO     search time limit: None
INFO     search memory limit: None
INFO     search command line string: /home/gus/symk_ros_ws/install/symk_ros/share/symk_ros/builds/release/bin/downward --search 'sym_bd()' --internal-plan-file sas_plan < output.sas
[t=0.000151s, 11400 KB] reading input...
[t=0.000304s, 11400 KB] done reading input!
[t=0.001420s, 11796 KB] Search Algorithm: Symbolic Bidirectional Uniform Cost Search
[t=0.001475s, 11796 KB] Building successor generator...done!
[t=0.001537s, 11796 KB] peak memory difference for successor generator creation: 0 KB
[t=0.001556s, 11796 KB] time for successor generation creation: 0.000007s
[t=0.001576s, 11796 KB] Variables: 9
[t=0.001595s, 11796 KB] FactPairs: 18
[t=0.001621s, 11796 KB] Bytes per state: 4
[t=0.001642s, 11796 KB] Mutex type changed to mutex_and because the domain has conditional effects, axioms and/or sdac.

[t=0.001668s, 11796 KB] CUDD Init: nodes=16000000 cache=16000000 max_memory=0
[t=0.001686s, 11796 KB] Variable Ordering: gamer
[t=0.001704s, 11796 KB] Dynamic reordering: False

[t=0.001723s, 11796 KB] TR(time=60000, nodes=100000, ce_type=conjunctive with early quantification)
[t=0.001741s, 11796 KB] Mutex(time=60000, nodes=100000, type=and)
[t=0.001759s, 11796 KB] Aux(time=2000, nodes=1000000)
[t=0.001776s, 11796 KB] Max alloted time (for bd): 60.000000s nodes: 10000000
[t=0.001798s, 11796 KB] Mult alloted time (for bd): 2.000000 nodes: 2.000000

[t=0.001827s, 11796 KB] building causal graph...done! [t=0.000035s]
[t=0.001873s, 11796 KB] Optimizing variable ordering...done! [t=0.037004s]
[t=0.038949s, 11796 KB] Sym variable order: 0 1 2 3 7 5 4 6 8 
[t=0.039071s, 11796 KB] Initializing Symbolic Variables
[t=0.039111s, 11796 KB] Num variables: 9 => 9
[t=0.039131s, 11796 KB] Initialize Symbolic Manager(18, 888888, 16000000, 0)
[t=0.253998s, 435820 KB] Generating binary variables
[t=0.254127s, 435820 KB] Symbolic Variables... Done.
[t=0.254158s, 435820 KB] Creating Primary Representation for Derived Predicates...
[t=0.254183s, 435820 KB] LAYER 0...done!
[t=0.254215s, 435820 KB] Symbolic Axiom initialization: 0.000055s
[t=0.254240s, 435820 KB] Primary Representation... Done!

[t=0.254289s, 435820 KB] Plan Selector: Top-K
[t=0.254317s, 435820 KB] Plan files: sas_plan

[t=0.254354s, 435820 KB] Creating 1 transition relations...Done!
[t=0.254380s, 435820 KB] Individual transition relations: 1 (disj=1, conj=0)
[t=0.254401s, 435820 KB] Merging transition relations...done!
[t=0.254424s, 435820 KB] Merged transition relations: 1 (disj=1, conj=0)
[t=0.254453s, 435820 KB] BOUND: 1 < 2147483647 [0/1 plans], total time: 0.254473s, 
[t=0.254665s, 435820 KB] BOUND: 2147483647 < 1 [1/1 plans], dir: FW, total time: 0.254694s, 

[t=0.254700s, 435820 KB] Actual search time: 0.000266s
[t=0.254721s, 435820 KB] Best plan:
party gustavo lucas albert einstein edson turtlebot rob2 livingroom (1)
[t=0.254721s, 435820 KB] Plan length: 1 step(s).
[t=0.254721s, 435820 KB] Plan cost: 1
[t=0.254721s, 435820 KB] Search time: 0.000432s
[t=0.254721s, 435820 KB] Total time: 0.254721s
Solution found.
Peak memory: 435820 KB
Remove intermediate file output.sas
search exit code: 0

INFO     Planner time: 278.27s

I don't think this is a particularly easy example due to the (inferred-party ?p ?p2 ?p3 ?p4 ?p5 ?r ?r2 ?room) derived predicate. It contains 7 variables with the object type, i.e., allowing all instances of the formulation to be used to solve it. In addition, the body of the derived predicate doesn't put many restrictions on combining the variables. This results in 644332 possible combinations.

The two main bottlenecks in the current approach are:

@tobiaswjohn Fortunately, for our use case this is much faster, it takes around 1s. It is not ideal, but ....

@tobiaswjohn
Copy link

Thank you for your comments. I think you made good progress and the time seems to be somehow usable for our use case (at least if we assume that executing an action takes several seconds anyways). At least, it should be small enough, that we can argue that it is not the center of our work to optimize this.
Also: @Rezenders is the 1s with the current domains, that contain a lot of "useless" derivation rules? I guess, we could get rid of most of the derived predicates, if I would spend the effort to optimize our translation tool, which should decrease the run time.

One thing I was wondering about, which could be an optimization: Should one maybe have a list of all predicates, for which no assignment exists? E.g. for our work, consider the example from the top:

(:derived (inferred-Fg_status ?fg ?IN_ERROR_NFR_string)
		(and
			(= ?IN_ERROR_NFR_string IN_ERROR_NFR_string)
			(exists (?mqa ?eqa ?mqav ?fd ?eqav)
 				(and
					(inferred-FunctionGrounding ?fg)
					(inferred-HasQAvalue ?fg ?mqa)
					(inferred-IsQAtype ?eqa water_visibility)
					(inferred-HasValue ?mqa ?mqav)
					(inferred-TypeFD ?fg ?fd)
					(inferred-HasValue ?eqa ?eqav)
					(inferred-HasQAestimation ?fd ?eqa)
					(inferred-IsQAtype ?mqa water_visibility)
					(inferred-FunctionDesign ?fd)
					(inferred-QAvalue ?mqa)
					(inferred-QAvalue ?eqa)
					(inferred-LessThan ?mqav ?eqav)
				)
			)
 		)
 	)

If we saved somewhere, that e.g. "inferred-FunctionDesign" is empty, i.e. no assertion with this predicate exists, then we don't need to evaluate any of the other predicates. I think, this could speed up the whole process quite a bit.
Similarly, we could have a list of the predicates for which new assertions were added in the last iteration of constructing D, i.e. if there is no predicate for which the assertions were updated --> do not evaluate any of the predicates, as this will not lead to new assertions.
To summarize: we should keep track of the predicates for which no assertions exist and the one for which new assertions where added in the last iteration of the loop. If an (and ...) or an (or ...) occurs, check that there is no predicate in the "empty" list and at least one in the "updated" list before doing the evaluation.

Just a vague idea, that might be not too complicated to implement. :) But I think, we can also focus on other things than optimizing this algorithm, if we want.

@Rezenders
Copy link
Contributor Author

Similarly, we could have a list of the predicates for which new assertions were added in the last iteration of constructing D, i.e. if there is no predicate for which the assertions were updated --> do not evaluate any of the predicates, as this will not lead to new assertions.
To summarize: we should keep track of the predicates for which no assertions exist and the one for which new assertions where added in the last iteration of the loop. If an (and ...) or an (or ...) occurs, check that there is no predicate in the "empty" list and at least one in the "updated" list before doing the evaluation.

In addition to this, when applying the effect of actions, we could keep track of the predicates inserted/removed and only reevaluate the derived predicates that could be impacted instead of reevaluating all derived predicates. Constantly reevaluating all derived predicates is a current bottleneck to the approach

@Rezenders
Copy link
Contributor Author

I had an idea: instead of saving the derived predicates as a vector, we could save it as a direct acyclic graph representing the dependencies between the derived predicates. For example:

The derived predicates in the following domain:

(:derived (inferred-InstanceA ?a)
  (and
    (= (?a instanceA))
  )
)

(:derived (inferred-A ?a)
  (and
    (predicateA ?a)
  )
)

(:derived (inferred-B ?b)
  (and
    (predicateB ?b)
  )
)

(:derived (inferred-AA ?a)
  (and
    (inferred-A ?a)
  )
)

(:derived (inferred-BB ?b)
  (and
    (inferred-B ?b)
  )
)

(:derived (inferred-AB ?a ?b)
  (and
    (inferred-A ?a)
    (inferred-B ?b)
  )
)

Could be represented with the following graph:

image

Then, instead of the current algorithm for solving derived predicates:

solveAllDerived(S):
   S' := derive(S)
   while (S != S'):
      S := S'
      S' := derive(S)
   return S

derive(S):
  for all d=(:derived (p ?x_1 ... ?x_n) C):
    E := evaluate(C, S)
    Derived := ground(p, E)
    S := union(S, Derived)
  return S

We could traverse the graph and solve the derived predicates in the correct order. This only requires one pass to solve all derived predicates, in contrast to the previous approach, which could take several iterations to finish. Furthermore, most critically, the last iteration from the previous approach doesn't solve any additional derived predicate. It only confirms that all predicates that could be solved were solved.

The graph search could be either breadth-first or depth-first, I don't think it makes any difference.

Apply effects

With the graph representation, when applying the effects of an action in a given state, we can check the graph to find out which derived predicates the effect impacts and only solve these derived predicates again. In the example above, if instances of predicateA are added or removed we would only need to solve inferred-A, inferred-AA, and inferred-AB.

Prune derived

Furthermore, since the end goal of solving the derived predicates is to use them to evaluate whether the actions being executed have their preconditions satisfied at execution time and to generate a plan-graph from the action plan, we could prune the derived predicate graph to contain only the derived predicates required for the actions in the current plan. For example:

Given the following actions and the pddl formulation above:

(:action actionA
  :parameters (?a)
  :precondition (and
    (inferred-A ?a)
  )
  :effect (and
    (predicateB ?a)
  )
)

(:action actionB
  :parameters (?b)
  :precondition (and
    (inferred-b ?b)
  )
  :effect (and
    (goal ?b)
  )
)

Suppose the current plan is:

(actionA instance)
(actionB instance)

The derived predicates graph could be pruned to:

image

@tobiaswjohn
Copy link

I like the idea with the graph! I think this an work well, as long as the set of grounded derived predicates is not too large.
But it also looks like quite some effort to implement. :D

@fmrico
Copy link
Contributor

fmrico commented Nov 8, 2024

Graph way looks promising :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants