pddl.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 import fcntl
4 import rospy
5 import os
6 import re
7 import signal
8 import subprocess as sp
9 import sys
10 import shlex
11 import tempfile
12 import traceback
13 
14 from pddl_msgs.msg import *
15 import actionlib
16 
17 
18 class ActionPreemptedException(Exception):
19  pass
20 
21 
22 class ParseError(Exception):
23  pass
24 
25 
26 def read_out(out, nonblock=True):
27  if nonblock:
28  fd = out.fileno()
29  fl = fcntl.fcntl(fd, fcntl.F_GETFL)
30  fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
31  try:
32  ret = out.read()
33  if ret == None:
34  return str()
35  else:
36  return ret
37  except:
38  return str()
39 
40 
42  _result = PDDLPlannerResult()
43  def __init__(self, name):
44  self._action_name = name
46  self._action_name, PDDLPlannerAction, self.execute_cb, auto_start=False)
47  # resolve rosparam
48  self._planner_name = rospy.get_param('~pddl_planner', 'downward')
49  search_option = rospy.get_param('~pddl_search_option', '')
50  # https://stackoverflow.com/questions/79968/split-a-string-by-spaces-preserving-quoted-substrings-in-python
51  # https://github.com/jsk-ros-pkg/jsk_planning/pull/88 changes 'sp.Popen(command' to 'sp.Popen(" ".join(command)', this assumes `planner_option` uses `--search &quat;iterated([lazy_greedy([hff,hlm], preferred=[hff,hlm]), ...)&quat;`
52  # but some launch file uses `--search iterated([lazy_greedy([hff,hlm],preferred=[hff,hlm]), ...)`, without &quat; and spaces, and jsk_planning 0.1.13 did not work this such eample(https://github.com/jsk-ros-pkg/jsk_demos/blob/ab0360b5580e77ca70006ce505497894fe4ac0d2/jsk_2013_04_pr2_610/test/test-demo-plan.test#L10), https://github.com/jsk-ros-pkg/jsk_demos/issues/1286
53  # this fix uses shlex.split() to keep quated substrings and uses Popen(command, stead of Popen(" ".join(command), to input quated argument as one word.
54  self._search_option = shlex.split(search_option.strip())
55 
56  self._as.start()
57 
58  def execute_cb(self, goal):
59  try:
60  problem = goal.problem
61  domain = goal.domain
62  max_planning_time = goal.max_planning_time.to_sec()
63  rospy.loginfo("take a message")
64  (problem_path, domain_path) = self.gen_tmp_pddl_file(problem, domain)
65  rospy.loginfo("problem_path => %s" % problem_path)
66  rospy.loginfo("domain_path => %s" % domain_path)
67  result = self.call_pddl_planner(problem_path, domain_path, max_planning_time)
68 
69  if self._planner_name == "lpg":
70  self._result.sequence = [PDDLStep(action = x.split(' ')[1].lstrip("\("),
71  args = re.search("\([^\)]+\)", x).group(0).lstrip("\(").rstrip("\)").split(' ')[1:],
72  start_time = re.search("[0-9]+.[0-9]+:" , x).group(0).rstrip(":"),
73  action_duration = re.search("\[D[^\)]+;", x).group(0).lstrip("[D:").rstrip(";")
74  )
75  for x in result]
76  self._result.use_durative_action = True
77  else:
78  self._result.sequence = [PDDLStep(action = x.split(' ')[0],
79  args = x.split(' ')[1:])
80  for x in result]
81  self._as.set_succeeded(self._result)
82  rospy.loginfo("action finished with success")
83  except ActionPreemptedException:
84  rospy.loginfo("action preempted")
85  self._as.set_preempted()
86  except ParseError:
87  rospy.logerr("Failed to parse output")
88  self._as.set_aborted()
89  except RuntimeError as e:
90  rospy.logerr("Planner exited with error: %s" % e)
91  self._as.set_aborted()
92  except Exception as e:
93  rospy.logerr("Unhandled Error: %s" % e)
94  rospy.logerr(traceback.format_exc())
95  self._as.set_aborted()
96 
97  def parse_pddl_result(self, output):
98  rospy.loginfo(output)
99  # dirty implementation
100  step_before_after = output.split("step")
101  if len(step_before_after) == 2:
102  results = [re.sub("\s*$", "", re.sub(r'^\s*\d+:\s*', "" , x))
103  for x in step_before_after[1].split("time spent")[0].split("\n")]
104  rospy.loginfo("result => %s" % results)
105  return filter(lambda x: x != "", results)
106  else:
107  raise ParseError()
108  def parse_pddl_result_ffha(self, output):
109  rospy.loginfo(output)
110  # dirty implementation
111  step_before_after = output.split("step")
112  if len(step_before_after) == 2:
113  results = [y.group(0)
114  for y in [re.search("\([^\)]+\)", x)
115  for x in step_before_after[1].split("Total cost")[0].split("\n")[1:]]
116  if y != None]
117  results = filter(lambda a: a.find("(REACH-GOAL)") < 0, results)
118  rospy.loginfo("result => %s" % results)
119  return results
120  else:
121  raise ParseError()
122 
123  def parse_pddl_result_lpg(self, output):
124  rospy.loginfo(output)
125  #dirty implementation
126  duration_before_after = output.split("action Duration")
127  if len(duration_before_after) == 2:
128  results = [y.group(0)
129  for y in [re.search("[0-9][^\]]+\]", x)
130  for x in duration_before_after[1].split("Solution number")[0].split("\n")[1:]]
131  if y != None]
132  rospy.loginfo("result => %s" % results)
133  return results
134  else:
135  raise ParseError()
136 
137  def parse_pddl_result_downward(self, path_name):
138  plan_path = path_name
139  i = 1
140  while os.path.exists(path_name + "." + str(i)):
141  plan_path = path_name + "." + str(i)
142  i += 1
143  rospy.loginfo("plan_path => %s" % plan_path)
144 
145  with open(plan_path) as f:
146  plan = f.read().split("\n")
147 
148  plan.remove("")
149  results = [re.sub(" \)$", ")", x)
150  for x in plan]
151  rospy.loginfo(results)
152 
153  return results
154 
155  def exec_process(self, command, max_planning_time):
156  if max_planning_time > 0.0:
157  command = ["timeout", str(max_planning_time)] + command
158  rospy.loginfo("Command: %s" % command)
159  proc = sp.Popen(command, stdout=sp.PIPE, stderr=sp.PIPE)
160  try:
161  output, error = str(), str()
162  r = rospy.Rate(10.0)
163  while True:
164  if rospy.is_shutdown():
165  return None
166  if self._as.is_preempt_requested():
168  if proc.poll() is not None:
169  break
170  # non-blocking read to avoid dead-lock
171  data_out = read_out(proc.stdout)
172  data_err = read_out(proc.stderr)
173  if type(data_out) == bytes:
174  data_out = data_out.decode('utf-8')
175  if type(data_err) == bytes:
176  data_err = data_err.decode('utf-8')
177  output += data_out
178  error += data_err
179  r.sleep()
180 
181  # flush output
182  data = proc.communicate()
183  data_out = data[0]
184  data_err = data[1]
185  if type(data_out) == bytes:
186  data_out = data_out.decode('utf-8')
187  if type(data_err) == bytes:
188  data_err = data_err.decode('utf-8')
189  output += data_out
190  error += data_err
191 
192  if proc.returncode not in [0, 124]:
193  # 0: normal exit
194  # 124: exited with timeout command
195  # others: process exited abnormally
196  msg = "Output:\n" + output + "\n"
197  msg += "Error:\n" + error + "\n"
198  msg += "Exit code: {}\n".format(proc.poll())
199  raise RuntimeError(msg)
200  return output
201  finally:
202  self.kill_process(proc)
203 
204  def kill_process(self, proc):
205  if proc is None or proc.poll() is not None:
206  return
207  try:
208  # 1. SIGINT
209  proc.send_signal(signal.SIGINT)
210  rospy.sleep(10.0)
211 
212  if proc.poll() is not None:
213  return
214 
215  # 2. Escalated to SIGTERM
216  proc.send_signal(signal.SIGTERM)
217  rospy.sleep(3.0)
218 
219  if proc.poll() is not None:
220  return
221 
222  # 3. Escalated to SIGKILL
223  proc.kill()
224  proc.wait()
225  except Exception as e:
226  rospy.logerr("Failed to kill process: %s" % e)
227 
228 
229  def call_pddl_planner(self, problem, domain, max_planning_time):
230 
231  if self._planner_name == "ff":
232  # -f problem -o domain
233  output = self.exec_process(["rosrun", "ff", "ff", "-f", problem, "-o", domain],
234  max_planning_time)
235  return self.parse_pddl_result(output)
236 
237  # ffha
238  elif self._planner_name == "ffha":
239  output = self.exec_process(["rosrun", "ffha", "ffha"] + self._search_option + ["-f", problem, "-o", domain],
240  max_planning_time)
241  if re.search("final domain representation is:", output):
242  tmp = output.split("metric:")
243  if len(tmp) > 1:
244  output = tmp[1];
245  self._result.data = tmp;
246  return self.parse_pddl_result_ffha(output)
247  # downward
248  elif self._planner_name == "downward":
249  (fd, path_name) = tempfile.mkstemp(text=True, prefix='plan_')
250  output = self.exec_process(["rosrun", "downward", "plan", domain, problem] + self._search_option + ["--plan-file", path_name],
251  max_planning_time)
252  rospy.loginfo(output)
253  self._result.data = output
254  return self.parse_pddl_result_downward(path_name)
255  # lpg
256  elif self._planner_name == "lpg":
257  output = self.exec_process(["rosrun", "lpg_planner", "lpg-1.2"] + self._search_option + ["-f", problem, "-o", domain],
258  max_planning_time)
259  return self.parse_pddl_result_lpg(output)
260 
261  else:
262  rospy.logfatal("set invalid planner: %s !" % self._planner_name)
263  return
264 
265  def gen_tmp_pddl_file(self, problem, domain):
266  search_durative = re.search("durative", domain.requirements)
267  rospy.loginfo ("gen_tmp_pddl_file: requirements:%s" % domain.requirements)
268  if search_durative == None:
269  problem_file = self.gen_tmp_problem_pddl_file(problem)
270  domain_file = self.gen_tmp_domain_pddl_file(domain)
271  return (problem_file, domain_file)
272  else:
273  problem_file = self.gen_tmp_problem_pddl_file(problem)
274  domain_file = self.gen_tmp_durative_domain_pddl_file(domain)
275  return (problem_file, domain_file)
276 
277  def gen_problem_objects_strings(self, objects):
278  # objects = list of PDDLObject
279  # PDDLObject has name and type
280  # collect PDDLObjects which has the same type
281  grouped_objects = {}
282  # grouped_objects := (type_and_objects ...)
283  # type_and_objects := (type_name object_a object_b ...)
284  # destructively change grouped_objects
285  for o in objects:
286  object_name = o.name
287  # find same object_type in grouped_objects
288  if o.type in grouped_objects:
289  grouped_objects[o.type].append(o.name)
290  else:
291  grouped_objects[o.type] = [o.name]
292  return [" ".join(grouped_objects[t]) + " - " + t
293  for t in grouped_objects.keys()]
294 
295  def gen_tmp_problem_pddl_file(self, problem):
296  (fd, path_name) = tempfile.mkstemp(text=True, prefix='problem_')
297  path = os.fdopen(fd, 'w')
298  path.write("""(define (problem %s)
299 (:domain %s)
300 (:objects %s)
301 (:init %s)
302 (:goal %s)
303 """ % (problem.name, problem.domain,
304  "\n".join(self.gen_problem_objects_strings(problem.objects)),
305  ' '.join(problem.initial), problem.goal))
306  if problem.metric:
307  path.write("""(:metric %s)""" % problem.metric)
308  path.write(""")""")
309  return path_name
310 
311  def gen_tmp_domain_pddl_file(self, domain):
312  (fd, path_name) = tempfile.mkstemp(text=True, prefix='domain_')
313  path = os.fdopen(fd, 'w')
314  path.write("(define (domain %s)\n" % domain.name)
315  path.write("(:requirements %s)\n" % domain.requirements)
316  path.write("(:types \n")
317  for i in domain.types:
318  path.write(i + " ")
319  path.write(")\n")
320  if len(domain.constants) > 0:
321  path.write("(:constants \n")
322  for i in domain.constants:
323  path.write(i + " ")
324  path.write(")\n")
325  path.write("(:predicates\n")
326  for i in domain.predicates:
327  path.write(i + " ")
328  path.write(")\n")
329  if domain.functions:
330  path.write("(:functions\n")
331  for i in domain.functions:
332  path.write(i + " ")
333  path.write(")\n")
334  for action in domain.actions:
335  path.write("(:action %s\n" % action.name)
336  path.write(":parameters %s\n" % action.parameters)
337  path.write(":precondition %s\n" % action.precondition)
338  path.write(":effect %s\n" % action.effect)
339  path.write(")\n") # (:action
340  path.write(")\n") # (define
341  return path_name
342 
344  rospy.loginfo("domain.actions:%s" % domain.actions)
345  (fd, path_name) = tempfile.mkstemp(text=True, prefix='domain_')
346  path = os.fdopen(fd, 'w')
347  path.write("(define (domain %s)\n" % domain.name)
348  path.write("(:requirements %s)\n" % domain.requirements)
349  path.write("(:types \n")
350  for i in domain.types:
351  path.write(i + " ")
352  path.write(")\n")
353  if len(domain.constants) > 0:
354  path.write("(:constants \n")
355  for i in domain.constants:
356  path.write(i + " ")
357  path.write(")\n")
358  path.write("(:predicates\n")
359  for i in domain.predicates:
360  path.write(i + " ")
361  path.write(")\n")
362  if domain.functions:
363  path.write("(:functions\n")
364  for i in domain.functions:
365  path.write(i + " ")
366  path.write(")\n")
367  for action in domain.actions:
368  path.write("(:durative-action %s\n" % action.name)
369  path.write(":parameters %s\n" % action.parameters)
370  path.write(":duration %s\n" % action.action_duration)
371  path.write(":condition %s\n" % action.precondition)
372  path.write(":effect %s\n" % action.effect)
373  path.write(")\n") # (:action
374  path.write(")\n") # (define
375  return path_name
376 
377 
378 if __name__ == '__main__':
379  rospy.init_node('pddl_planner')
380  PDDLPlannerActionServer(rospy.get_name())
381  rospy.spin()
pddl.read_out
def read_out(out, nonblock=True)
Definition: pddl.py:26
pddl.PDDLPlannerActionServer.exec_process
def exec_process(self, command, max_planning_time)
Definition: pddl.py:155
pddl.PDDLPlannerActionServer.parse_pddl_result
def parse_pddl_result(self, output)
Definition: pddl.py:97
solve-hanoi.type
type
Definition: solve-hanoi.py:72
pddl.ParseError
Definition: pddl.py:22
msg
pddl.PDDLPlannerActionServer._action_name
_action_name
Definition: pddl.py:44
pddl.PDDLPlannerActionServer.execute_cb
def execute_cb(self, goal)
Definition: pddl.py:58
pddl.PDDLPlannerActionServer.gen_tmp_problem_pddl_file
def gen_tmp_problem_pddl_file(self, problem)
Definition: pddl.py:295
pddl.PDDLPlannerActionServer.gen_tmp_durative_domain_pddl_file
def gen_tmp_durative_domain_pddl_file(self, domain)
Definition: pddl.py:343
pddl.PDDLPlannerActionServer.gen_problem_objects_strings
def gen_problem_objects_strings(self, objects)
Definition: pddl.py:277
pddl.PDDLPlannerActionServer._search_option
_search_option
Definition: pddl.py:54
pddl.PDDLPlannerActionServer.parse_pddl_result_downward
def parse_pddl_result_downward(self, path_name)
Definition: pddl.py:137
pddl.PDDLPlannerActionServer.parse_pddl_result_ffha
def parse_pddl_result_ffha(self, output)
Definition: pddl.py:108
pddl.ActionPreemptedException
Definition: pddl.py:18
pddl.PDDLPlannerActionServer.__init__
def __init__(self, name)
Definition: pddl.py:43
pddl.PDDLPlannerActionServer.gen_tmp_pddl_file
def gen_tmp_pddl_file(self, problem, domain)
Definition: pddl.py:265
pddl.PDDLPlannerActionServer._as
_as
Definition: pddl.py:45
pddl.PDDLPlannerActionServer.call_pddl_planner
def call_pddl_planner(self, problem, domain, max_planning_time)
Definition: pddl.py:229
pddl.PDDLPlannerActionServer._result
_result
Definition: pddl.py:42
pddl.PDDLPlannerActionServer.parse_pddl_result_lpg
def parse_pddl_result_lpg(self, output)
Definition: pddl.py:123
pddl.PDDLPlannerActionServer.gen_tmp_domain_pddl_file
def gen_tmp_domain_pddl_file(self, domain)
Definition: pddl.py:311
pddl.PDDLPlannerActionServer
Definition: pddl.py:41
actionlib::SimpleActionServer
pddl.PDDLPlannerActionServer._planner_name
_planner_name
Definition: pddl.py:48
pddl.PDDLPlannerActionServer.kill_process
def kill_process(self, proc)
Definition: pddl.py:204


pddl_planner
Author(s): Ryohei Ueda (ueda@jsk.t.u-tokyo.ac.jp)
autogenerated on Fri Dec 8 2023 03:35:32