├── .idea └── workspace.xml ├── CMakeLists.txt ├── LTLMP ├── CMakeLists.txt ├── __init__.py ├── __init__.pyc ├── msg │ ├── activity.msg │ ├── activity_pose.msg │ ├── confirmation.msg │ ├── knowledge.msg │ └── pose.msg └── src │ ├── RTLTL.py │ ├── __init__.py │ ├── formula_parser │ ├── __init__.py │ ├── lexer.py │ ├── ltlf2dfa.py │ ├── ltlf_parser.py │ └── parser.py │ ├── gazebo_HMI_plugin.py │ ├── ros_gazebo_HMI_base.py │ └── utils │ ├── __init__.py │ ├── misc_func.py │ ├── naive_evaluator.py │ ├── pyb_plan_utils.py │ ├── tree_visualizer.py │ └── workspace.py ├── figure └── coverpage.png └── readme.md /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 20 | 21 | 22 | 24 | 25 | 27 | 28 | 29 | 30 | 33 | { 34 | "keyToString": { 35 | "RunOnceActivity.OpenProjectViewOnStart": "true", 36 | "RunOnceActivity.ShowReadmeOnStart": "true", 37 | "WebServerToolWindowFactoryState": "false", 38 | "last_opened_file_path": "/home/joseph/yzchen_ws/task_planning/ltl_mq_opensource/src", 39 | "settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings" 40 | } 41 | } 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 70 | 71 | 72 | 92 | 93 | 94 | 95 | 96 | 97 | 1690950025846 98 | 116 | 117 | 118 | 119 | 121 | 122 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | /opt/ros/melodic/share/catkin/cmake/toplevel.cmake -------------------------------------------------------------------------------- /LTLMP/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.3) 2 | project(rtltl) 3 | 4 | 5 | find_package(catkin REQUIRED COMPONENTS 6 | roscpp 7 | rospy 8 | std_msgs 9 | message_generation 10 | geometry_msgs 11 | ) 12 | 13 | 14 | ## Generate messages in the 'msg' folder 15 | add_message_files( 16 | FILES 17 | # activity.msg 18 | confirmation.msg 19 | pose.msg 20 | knowledge.msg 21 | activity_pose.msg 22 | ) 23 | 24 | 25 | ## Generate added messages and services with any dependencies listed here 26 | generate_messages( 27 | DEPENDENCIES 28 | std_msgs 29 | geometry_msgs 30 | # activity.msg 31 | ) 32 | 33 | catkin_package( 34 | # INCLUDE_DIRS include 35 | # LIBRARIES ltl3 36 | CATKIN_DEPENDS roscpp rospy std_msgs 37 | # DEPENDS system_lib 38 | geometry_msgs 39 | ) 40 | 41 | ########### 42 | ## Build ## 43 | ########### 44 | 45 | ## Specify additional locations of header files 46 | ## Your package locations should be listed before other locations 47 | # include_directories(include) 48 | include_directories( 49 | ${catkin_INCLUDE_DIRS} 50 | ) 51 | 52 | -------------------------------------------------------------------------------- /LTLMP/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JINXER000/multiple-task-temporal-logic-planning/16bc21cac81b733efd0e657791ac492d2f0696fc/LTLMP/__init__.py -------------------------------------------------------------------------------- /LTLMP/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JINXER000/multiple-task-temporal-logic-planning/16bc21cac81b733efd0e657791ac492d2f0696fc/LTLMP/__init__.pyc -------------------------------------------------------------------------------- /LTLMP/msg/activity.msg: -------------------------------------------------------------------------------- 1 | int64 header 2 | string type 3 | float64 x 4 | float64 y 5 | float64 psi 6 | -------------------------------------------------------------------------------- /LTLMP/msg/activity_pose.msg: -------------------------------------------------------------------------------- 1 | int64 header 2 | string type 3 | geometry_msgs/PoseStamped tgt_pose 4 | -------------------------------------------------------------------------------- /LTLMP/msg/confirmation.msg: -------------------------------------------------------------------------------- 1 | int64 confheader 2 | string name 3 | int8 done 4 | string info 5 | -------------------------------------------------------------------------------- /LTLMP/msg/knowledge.msg: -------------------------------------------------------------------------------- 1 | string object 2 | int8 source 3 | float64 x 4 | float64 y 5 | -------------------------------------------------------------------------------- /LTLMP/msg/pose.msg: -------------------------------------------------------------------------------- 1 | float64 x 2 | float64 y 3 | float64 psi 4 | -------------------------------------------------------------------------------- /LTLMP/src/RTLTL.py: -------------------------------------------------------------------------------- 1 | 2 | import networkx as nx 3 | import numpy as np 4 | from networkx.classes.digraph import DiGraph 5 | from utils.misc_func import get_automaton_dict, LineSegment, LinesegIntersectsAABB, getLabel, \ 6 | isWithinAABB, randomPointAABB, pybCheckCollision, collisionFreeAABB, ptInObs 7 | 8 | import pybullet as p 9 | from random import uniform 10 | import rospy 11 | 12 | class RT_LTL_planner(): 13 | class Node(): 14 | def __init__(self, node_id, pos, cost2Root, b_state, label): 15 | self.node_id = node_id 16 | self.pos = pos 17 | self.cost2Root = cost2Root 18 | self.b_state = b_state 19 | self.label = label 20 | self.same_pos_nodes =set() 21 | self.visited = False 22 | 23 | def __hash__(self): 24 | return hash(self.node_id) 25 | 26 | def __eq__(self, other): 27 | return (self.__class__ == other.__class__ and self.node_id == other.node_id) 28 | 29 | def __init__(self, root, goal, workspace, evaluator, para, automaton, segment, is_hybrid = False): 30 | self.para = para 31 | 32 | self.tree = DiGraph(init_pose = root.pos) 33 | self.tree.add_node(root) 34 | self.init_state = root 35 | self.goal = goal 36 | self.nodesAroundGoal = [] 37 | self.goalFound = False 38 | self.evaluator = evaluator 39 | # workspace 40 | self.workspace = workspace 41 | self.dim = len(self.workspace.workspace_range) 42 | 43 | # variables for real-time 44 | self.rt_root = self.init_state 45 | self.cur_target = None 46 | self.sparse_r = 5 47 | self.cur_path = [] 48 | self.rand_near_nbrs = set() 49 | self.rewireRand = set() 50 | self.visited_set = set() 51 | self.rewireRoot = set() 52 | self.pushedToRewireRoot = set() # has been pushed to rewireRoot within current iter 53 | self.next_wp = None 54 | self.old_dynamic_obs = (-10, -10) 55 | self.old_dynobs_aabb = None 56 | 57 | # variables for LTL 58 | aut_dict = get_automaton_dict(automaton) 59 | self.rt_root.b_state = aut_dict[self.rt_root.b_state] 60 | self.automaton = self.unifyAutomata(automaton, aut_dict) 61 | self.accept = self.automaton.graph['accept'] 62 | self.segment = segment 63 | self.unreachable_b_states = self.preprocessDFA() 64 | 65 | 66 | self.possible_paths = [] 67 | self.timeKeeper = None 68 | self.detect_obs_move = False 69 | self.detect_TS_change = False 70 | # for node_id, only increase. The reason for designing this parameter is that 71 | # if using len(self.tree.nodes()), some nodes will be deleted, and id will be duplicated. 72 | self.added_nodes_cnt = 1 73 | self.task_initialized = True 74 | self.rbt_pos = self.rt_root.pos 75 | self.root_just_changed = False 76 | 77 | self.bad_regs = set() 78 | self.dist_root2next = 0 79 | self.dist_root2rbt = 0 80 | self.dist_next2rbt = 0 81 | 82 | self.is_hybrid = is_hybrid 83 | self.changed_regs = set() 84 | self.nodes_pos_dict = {root.pos: root} 85 | 86 | self.old_dynobs_AABB = None 87 | 88 | 89 | 90 | def unifyAutomata(self, automaton, aut_nodes_dict): 91 | 92 | renamed_graph = automaton.copy() 93 | renamed_graph = nx.relabel_nodes(renamed_graph, aut_nodes_dict) 94 | 95 | initial_set = set() 96 | for nd in renamed_graph.graph['initial']: 97 | initial_set.add(aut_nodes_dict[nd]) 98 | renamed_graph.graph['initial'] = list(initial_set) 99 | 100 | accept_set = set() 101 | for nd in renamed_graph.graph['accept']: 102 | accept_set.add(aut_nodes_dict[nd]) 103 | renamed_graph.graph['accept'] = list(accept_set) 104 | 105 | return renamed_graph 106 | 107 | def reverseVisit(self, b_node, reachable_set): 108 | reachable_set.add(b_node) 109 | 110 | for b_parent in self.automaton.pred[b_node]: 111 | if b_parent == b_node: 112 | continue 113 | if b_parent in reachable_set: 114 | continue 115 | self.reverseVisit(b_parent, reachable_set) 116 | 117 | # mark unreachable nodes 118 | def preprocessDFA(self): 119 | for ac_node in self.accept: 120 | 121 | reachable_set = set() 122 | self.reverseVisit(ac_node, reachable_set) 123 | 124 | all_b_nodes = set(self.automaton.nodes()) 125 | unreachable = all_b_nodes.difference(reachable_set) 126 | 127 | return unreachable 128 | 129 | 130 | 131 | 132 | def checkCrossLabel(self, node, x_new, new_label): 133 | # not changing b_state if crossing 134 | for (region, aabb) in iter(self.workspace.regions.items()): 135 | j1 = LinesegIntersectsAABB(aabb, LineSegment(node.pos, x_new)) 136 | 137 | inspect_label = getLabel(self.workspace.reg_centers[region], self.workspace, self.is_hybrid) 138 | j2 = inspect_label!= new_label 139 | j3 = inspect_label!= node.label 140 | if j1 and j2 and j3: 141 | return False 142 | return True 143 | 144 | 145 | def checkBuchiTran(self, q_b, x_label, q_b_new): 146 | 147 | if q_b is None or q_b_new is None or\ 148 | q_b not in self.automaton.nodes or q_b_new not in self.automaton.nodes: 149 | return False 150 | 151 | b_state_succ = self.automaton.succ[q_b] 152 | if q_b_new not in b_state_succ: 153 | return False 154 | 155 | tran_valid = self.automaton.edges[q_b, q_b_new]['guard'].check(x_label) 156 | # bad states 157 | is_astray = q_b_new in self.unreachable_b_states or q_b in self.unreachable_b_states 158 | 159 | ok = tran_valid and not is_astray 160 | return ok 161 | 162 | def near_succ(self, inspect_node): 163 | near_nodes = set() 164 | checked_set = set([inspect_node.pos]) 165 | cost_dict = {(inspect_node.pos, inspect_node.pos): 0} 166 | radius = self.near_dist 167 | 168 | for node in self.tree.nodes: 169 | if self.evaluator.euclideanDist(inspect_node.pos, node.pos) > radius: 170 | continue 171 | 172 | if node.b_state not in self.automaton.succ[inspect_node.b_state]: 173 | continue 174 | 175 | # check trans 176 | trans_ok = self.checkBuchiTran(inspect_node.b_state, inspect_node.label, node.b_state) 177 | if not trans_ok: 178 | continue 179 | 180 | # for DFA, once state is accepting, we don't care its outward edge 181 | acc2other = inspect_node.b_state in self.accept and node.b_state not in self.accept 182 | if acc2other: 183 | continue 184 | 185 | if node.pos not in checked_set: 186 | # check obs 187 | no_collision = collisionFreeAABB(node.pos, inspect_node.pos, self.workspace, self.old_dynobs_aabb) 188 | safe_label = self.checkCrossLabel(node, inspect_node.pos, inspect_node.label) 189 | if not (no_collision and safe_label): 190 | continue 191 | 192 | cost_dict[(inspect_node.pos, node.pos)] = self.evaluator.getCost(node, inspect_node.pos) 193 | checked_set.add(node.pos) 194 | 195 | near_nodes.add(node) 196 | 197 | return near_nodes, cost_dict 198 | 199 | 200 | def extend(self, new_node, near_nodes, cost_dict): 201 | added = False 202 | min_cost = 1e7 203 | best_nbr = () 204 | 205 | # near_nodes are nodes that are collision-free, and have distinct position 206 | for diff_x_node in near_nodes: 207 | # iterate over S^{rcd} 208 | for node in diff_x_node.same_pos_nodes: 209 | if node.node_id == new_node.node_id: 210 | continue 211 | 212 | j2 = self.checkBuchiTran(node.b_state, node.label, new_node.b_state) 213 | # don't go from accept to non-accept 214 | j3 = node.b_state in self.accept and new_node.b_state not in self.accept 215 | 216 | if j2 and not j3: 217 | cost = self.calc_cost2Root(node) + cost_dict[(new_node.pos, node.pos)] 218 | if cost < min_cost: 219 | added = True 220 | best_nbr = node 221 | min_cost = cost 222 | 223 | if added: 224 | new_node.cost2Root=min_cost 225 | self.addNode(new_node, best_nbr) 226 | 227 | return added 228 | 229 | def rewire(self, new_node, near_nodes, cost_dict): 230 | rewired_nodes = set() 231 | for node in near_nodes: 232 | if node.node_id == new_node.node_id: 233 | continue 234 | # if b_state is None, assign new 235 | if node.cost2Root == np.inf and node.b_state is None: 236 | raise NotImplementedError() 237 | else: 238 | # note: as near_nodes are produced by near_succ(), there is no need to do checkBuchiTran() 239 | 240 | cost = self.calc_cost2Root(new_node) + cost_dict[(new_node.pos, node.pos)] 241 | delta_c = self.calc_cost2Root(node) - cost 242 | 243 | if delta_c > 1e-3: 244 | pred_node_list = list(self.tree.pred[node].keys()) 245 | if len(pred_node_list): 246 | pred_node = pred_node_list[0] 247 | self.tree.remove_edge(pred_node, node) 248 | # else: isolated node 249 | node.cost2Root = cost 250 | self.tree.add_edge(new_node, node) 251 | 252 | rewired_nodes.add(node) 253 | return rewired_nodes 254 | 255 | # one step further on automaton. 256 | def checkNode2Goal(self, node, is_hybrid, approx=False): 257 | if self.segment == 'suffix': 258 | raise 'Not Implemented' 259 | 260 | # filter out nodes blocked by dyn obs and isolated nodes 261 | nd_cost = self.calc_cost2Root(node) 262 | if nd_cost == np.inf: 263 | return False 264 | 265 | goal_updated = False 266 | 267 | if node.b_state in self.accept: 268 | pred_nodes = list(self.tree.pred[node].keys()) 269 | # if root satisfy LTL 270 | if len(pred_nodes) == 0: 271 | goal_updated = self.update_target(node) 272 | return goal_updated 273 | # else use the previous as the goal 274 | else: 275 | q_n = pred_nodes[0] 276 | if q_n.b_state not in self.accept: 277 | goal_updated = self.update_target(q_n) 278 | # accept -->(1)--> accept 279 | return goal_updated 280 | 281 | # node.b_state not in self.accept 282 | for b_accept in self.accept: 283 | nd_label = getLabel(node.pos, self.workspace, is_hybrid) 284 | if self.checkBuchiTran(node.b_state, nd_label, b_accept): 285 | goal_updated = self.update_target(node) 286 | 287 | return goal_updated 288 | 289 | def pos_near(self, pos1, pos2, dist): 290 | if self.evaluator.euclideanDist(np.array(pos1), np.array(pos2)) < dist: 291 | return True 292 | else: 293 | return False 294 | 295 | # similar to homotopy path 296 | def is_same_solution(self, sol1, sol2): 297 | if not len(sol1) or not len(sol2): 298 | raise 'invalid input' 299 | #1 if the ending point is different 300 | if not self.pos_near(sol1[-1].pos,sol2[-1].pos, 20): 301 | return False 302 | 303 | #2 check the changing point list: different length or shape 304 | if len(sol1) != len(sol2): 305 | return False 306 | 307 | for i in range(len(sol1)): 308 | if sol1[i].label != sol2[i].label or not self.pos_near(sol1[i].pos,sol2[i].pos, self.para['sameSol_radius']): 309 | return False 310 | 311 | return True 312 | 313 | def retrace_path(self, goal_node): 314 | path = [goal_node] 315 | pred_ls = list(self.tree.pred[goal_node].keys()) 316 | s = goal_node 317 | while len(pred_ls)!=0: 318 | s = self.tree.pred[s].keys()[0] 319 | path.insert(0, s) 320 | pred_ls = list(self.tree.pred[s].keys()) 321 | return path 322 | 323 | # find similar path; if found, update it; if not found, put the tgt into pq. 324 | # if root.automaton changed, try to rewire and check reachability. 325 | def update_target(self, node): 326 | updated = False 327 | if node.b_state is None: 328 | raise KeyError() 329 | 330 | node_path = self.retrace_path(node) 331 | node_trace = self.extract_trace(node_path) 332 | if len(node_trace) == 0: 333 | return False 334 | 335 | if self.cur_target is None: 336 | self.cur_target = node 337 | self.goalFound = True 338 | self.possible_paths.append(node_path) 339 | return True 340 | 341 | node_cost = self.calc_cost2Root(node) 342 | 343 | is_similar = False 344 | for i in range(len(self.possible_paths)): 345 | path_cmp = self.possible_paths[i] 346 | trace_cmp = self.extract_trace(path_cmp) 347 | is_similar = self.is_same_solution(trace_cmp, node_trace) 348 | 349 | if is_similar: 350 | # compare cost 351 | end_node = trace_cmp[-1] 352 | if node_cost < self.calc_cost2Root(end_node): 353 | # assign address? 354 | self.possible_paths[i] = node_path 355 | updated = True 356 | 357 | # can be similar but not that good 358 | break 359 | 360 | if not is_similar: 361 | self.possible_paths.append(node_path) 362 | updated = True 363 | 364 | if updated: 365 | self.possible_paths = sorted(self.possible_paths, key=lambda trace: self.calc_cost2Root(trace[-1])) 366 | self.cur_target = self.possible_paths[0][-1] 367 | 368 | return updated 369 | 370 | def calc_cost2Root(self, node): 371 | # next_wp is the R_aux 372 | is_pass_nxt = False 373 | is_bad_node = node.cost2Root == np.inf 374 | cost = 0 375 | pred_ls = list(self.tree.pred[node].keys()) 376 | s = node 377 | while len(pred_ls) != 0: 378 | if self.next_wp is not None and self.next_wp == s: 379 | is_pass_nxt = True 380 | 381 | parent = self.tree.pred[s].keys()[0] 382 | # check parent is valid 383 | if parent.cost2Root == np.inf: 384 | is_bad_node = True 385 | 386 | # check automaton tran 387 | parent_label = parent.label 388 | parent_bs = parent.b_state 389 | child_bs = s.b_state 390 | 391 | tran_valid = self.checkBuchiTran(parent_bs, parent_label, child_bs) 392 | if not tran_valid: 393 | s.cost2Root = np.inf 394 | is_bad_node = True 395 | 396 | cost += self.evaluator.euclideanDist(s.pos, parent.pos) 397 | s = parent 398 | pred_ls = list(self.tree.pred[s].keys()) 399 | 400 | if is_bad_node: 401 | node.cost2Root = np.inf 402 | return np.inf 403 | else: 404 | if is_pass_nxt: 405 | cost -= self.dist_root2next 406 | 407 | node.cost2Root = cost 408 | return cost 409 | 410 | # check the trace satisfy the specification 411 | def examine_trace(self, path): 412 | 413 | # implicitely check transition 414 | path_cost = self.calc_cost2Root(path[-1]) 415 | 416 | if path_cost == np.inf: 417 | return False 418 | 419 | last_bs = path[-1].b_state 420 | last_label = path[-1].label 421 | for b_accept in self.accept: 422 | if self.checkBuchiTran(last_bs, last_label, b_accept): 423 | return True 424 | 425 | return False 426 | 427 | # get event-driven trace 428 | def extract_trace(self, path): 429 | if len(path) <2: 430 | return path 431 | 432 | trace = [] # no starting point 433 | for id in range(0,len(path)-1): 434 | if path[id].label!= path[id+1].label: 435 | trace.append(path[id]) 436 | 437 | trace.append(path[-1]) 438 | return trace 439 | 440 | def ntr_node(self, better_node, worse_node): 441 | 442 | # rewire worse node 443 | succ_worse_nodes = list(self.tree.succ[worse_node]) 444 | for succ_worse_node in succ_worse_nodes: 445 | # rewire then do dfs on tree2 446 | self.tree.remove_edge(worse_node, succ_worse_node) 447 | self.tree.add_edge(better_node, succ_worse_node) 448 | 449 | prev_worse_node = list(self.tree.pred[worse_node])[0] 450 | self.tree.remove_edge(prev_worse_node, worse_node) 451 | worse_node.cost2Root = np.inf 452 | self.safe_remove_node(worse_node) 453 | 454 | # replace worse nodes in possible path 455 | for path in self.possible_paths: 456 | if path[-1] == worse_node: 457 | path[-1] = better_node 458 | 459 | def safe_remove_node(self, node): 460 | 461 | if len(node.same_pos_nodes): 462 | node.same_pos_nodes.remove(node) 463 | 464 | same_pos_nd_remain = None 465 | if len(node.same_pos_nodes): 466 | same_pos_nd_remain = list(node.same_pos_nodes)[0] 467 | 468 | # take care of pos_node_dict 469 | if self.nodes_pos_dict[node.pos] == node: 470 | 471 | if same_pos_nd_remain is None: 472 | # delete the entry 473 | self.nodes_pos_dict.pop(node.pos) 474 | else: 475 | self.nodes_pos_dict[node.pos] = same_pos_nd_remain 476 | 477 | self.tree.remove_node(node) 478 | 479 | def addNode(self, node, parent = None): 480 | self.tree.add_node(node) 481 | self.nodes_pos_dict[node.pos] = node 482 | self.added_nodes_cnt +=1 483 | 484 | if parent: 485 | self.tree.add_edge(parent, node) 486 | 487 | def isIsolated(self, node): 488 | if self.tree.in_degree(node) ==0 and self.tree.out_degree(node) ==0: 489 | return True 490 | else: 491 | return False 492 | 493 | def mergeNode(self, s_m, depth): 494 | better_node = s_m 495 | 496 | # pre-order 497 | same_state_nodes = [nd for nd in s_m.same_pos_nodes \ 498 | if nd.b_state == s_m.b_state and nd != s_m] # and len(list(self.tree.pred[nd]))!=0 499 | 500 | same_cpy = same_state_nodes[:] 501 | # delete isolated 502 | for nd in same_cpy: 503 | if self.isIsolated(nd): 504 | same_state_nodes.remove(nd) 505 | self.safe_remove_node(nd) 506 | 507 | same_state_num = len(same_state_nodes) 508 | 509 | if same_state_num >=1: 510 | # deal with father-son relation first. father cannot be bad. 511 | same_state_node = same_state_nodes[0] 512 | if same_state_node == list(self.tree.pred[s_m].keys())[0]: 513 | worse_node = s_m 514 | better_node = same_state_node 515 | else: 516 | worse_node = same_state_node 517 | 518 | if self.calc_cost2Root(s_m) > self.calc_cost2Root(same_state_node) and self.calc_cost2Root(same_state_node) <1e6: 519 | better_node = same_state_node 520 | worse_node = s_m 521 | 522 | self.ntr_node(better_node, worse_node) 523 | 524 | 525 | worse_node.visited = True 526 | 527 | better_node.visited = True 528 | 529 | better_succ_list = list(self.tree.succ[better_node]) 530 | # NOTE: better_succ_list can contain worse nodes which is deleted by its siblings. visited flag for the worse nodes are true. 531 | for child in better_succ_list: 532 | if child == self.rt_root: 533 | continue 534 | 535 | # child was visited as a worse node. no succ already. 536 | if child.visited: 537 | continue 538 | 539 | self.mergeNode(child, depth+1) 540 | 541 | 542 | # 2 step: 1, dfs b_state; 2. do the merging 543 | def merge_branch(self, subtree_root, parent): 544 | 545 | trans_valid = self.propagate_b_state(parent, subtree_root) 546 | if not trans_valid: 547 | return 548 | 549 | print('merge branch rooted at '+ str(subtree_root.pos)) 550 | 551 | self.propagateState(subtree_root, 0) 552 | 553 | # DFS post-order on subtree_root. 554 | self.mergeNode(subtree_root, 0) 555 | 556 | 557 | def propagateState(self, subroot, depth): 558 | 559 | succ_list = list(self.tree.succ[subroot]) 560 | # for merge_branch 561 | subroot.visited = False 562 | 563 | # NOTE: succ_list can contain worse nodes which is deleted by its siblings 564 | for child in succ_list: 565 | if child == self.rt_root: 566 | continue 567 | 568 | # can revise b_satate here 569 | trans_valid = self.propagate_b_state(subroot, child) 570 | if not trans_valid: 571 | continue 572 | 573 | self.propagateState(child, depth+1) 574 | 575 | def propagate_b_state(self, parent, child): 576 | 577 | 578 | child_bs_valid = self.checkBuchiTran(parent.b_state, parent.label, child.b_state) 579 | if child_bs_valid: 580 | return True 581 | 582 | buchi_succ = self.automaton.succ[parent.b_state] 583 | buchi_candidates = [] 584 | 585 | for b_state in buchi_succ: 586 | if b_state != child.b_state and self.checkBuchiTran(parent.b_state, parent.label, b_state): 587 | buchi_candidates.append(b_state) 588 | if len(buchi_candidates)>1: # not a DFA! 589 | raise NameError('too many candidates') 590 | elif len(buchi_candidates) ==1: 591 | 592 | # add old state, in case of rewire it back 593 | same_state_nodes =[nd for nd in child.same_pos_nodes 594 | if nd.b_state== child.b_state and nd != child] 595 | if len(same_state_nodes)==0: 596 | old_id = self.added_nodes_cnt 597 | old_state_node = self.Node(old_id, child.pos, np.inf, child.b_state, child.label) 598 | self.addNode(old_state_node) 599 | 600 | child.same_pos_nodes.add(old_state_node) 601 | old_state_node.same_pos_nodes = child.same_pos_nodes # addr of a list 602 | 603 | child.b_state = buchi_candidates[0] 604 | child_bs_valid = True 605 | else: # if DFA, won't enter. for safety LTL only. 606 | child.cost2Root = np.inf 607 | 608 | return child_bs_valid 609 | 610 | 611 | def updateBestPath(self): 612 | updated = False 613 | 614 | # has been sorted in validate_update_sols() 615 | possible_new_tgt = self.possible_paths[0][-1] 616 | if possible_new_tgt!= self.cur_target: 617 | updated = True 618 | self.cur_target = possible_new_tgt 619 | 620 | self.cur_path = self.retrace_path(self.cur_target) 621 | if len(self.cur_path)>1: 622 | self.next_wp = self.cur_path[1] 623 | self.dist_root2next = self.evaluator.euclideanDist(self.next_wp.pos, self.rt_root.pos) 624 | else: 625 | self.next_wp = self.cur_path[0] 626 | self.dist_root2next = 0.0 627 | return updated 628 | 629 | def validate_update_sols(self): 630 | # check if the all candidates are valid and leading to accept (cur_path should be the 1st of candidates) 631 | resume_path = False 632 | 633 | valid_paths = [] 634 | for path in self.possible_paths: 635 | updated_path= self.retrace_path(path[-1]) 636 | if self.examine_trace(updated_path): 637 | valid_paths.append(updated_path) 638 | 639 | if len(valid_paths)==0: 640 | self.codeRestart() 641 | return resume_path 642 | 643 | self.possible_paths = sorted(valid_paths, key=lambda trace: self.calc_cost2Root(trace[-1])) 644 | # update cur_target, cur_path 645 | self.updateBestPath() 646 | self.goalFound = True 647 | 648 | resume_path = True 649 | 650 | return resume_path 651 | 652 | def changeRoot(self, new_root): 653 | require_prop = False 654 | self.tree.remove_edge(self.rt_root, new_root) 655 | self.rt_root.cost2Root = new_root.cost2Root 656 | new_root.cost2Root = 0 657 | 658 | self.tree.add_edge(new_root, self.rt_root) 659 | 660 | reverse_trans_valid = self.checkBuchiTran(new_root.b_state, new_root.label, self.rt_root.b_state) 661 | if not reverse_trans_valid: 662 | require_prop = True 663 | 664 | self.rt_root = new_root 665 | 666 | return require_prop 667 | 668 | 669 | 670 | def get_bad_regs(self): 671 | self.bad_regs.clear() 672 | buchi_succ = self.automaton.succ[self.rt_root.b_state] 673 | cur_b_state = self.rt_root.b_state 674 | for b_state in buchi_succ: 675 | if self.checkBuchiTran(self.rt_root.b_state, self.rt_root.label, b_state): 676 | cur_b_state = b_state 677 | break 678 | 679 | b_state_succ = self.automaton.succ[cur_b_state] 680 | for bad_b in self.unreachable_b_states: 681 | if bad_b not in b_state_succ: 682 | continue 683 | 684 | for reg in self.workspace.regions: 685 | prop = self.workspace.properties[reg] 686 | lead_to_bad = self.automaton.edges[cur_b_state, bad_b]['guard'].check(prop) 687 | if lead_to_bad: 688 | self.bad_regs.add(reg) 689 | 690 | 691 | 692 | 693 | def sudden_stop(self): 694 | require_prop = False 695 | 696 | exactly_on_root = self.dist_root2rbt< self.para['changeRoot_radius'] 697 | exactly_on_nxt = self.goalFound and self.dist_next2rbt < self.para['changeRoot_radius'] 698 | if exactly_on_root: 699 | add_node_pos = self.rt_root.pos 700 | self.dist_root2rbt = 0 701 | elif exactly_on_nxt: # if meet obs and no valid sols, don't enter this condition 702 | add_node_pos = self.next_wp.pos 703 | require_prop = self.changeRoot(self.next_wp) 704 | self.dist_root2rbt = 0 705 | else: 706 | add_node_pos = tuple(self.rbt_pos) 707 | 708 | # if property change no affect, then will not merge branch. 709 | curpos_id = self.added_nodes_cnt 710 | cur_label = getLabel(add_node_pos, self.workspace, self.is_hybrid) 711 | 712 | if self.goalFound: 713 | prev_b_state = self.next_wp.b_state 714 | else: 715 | prev_b_state = self.rt_root.b_state 716 | 717 | curpos_node = self.Node(curpos_id, add_node_pos, self.dist_root2rbt, prev_b_state, cur_label) 718 | if not exactly_on_root and not exactly_on_nxt: 719 | curpos_node.same_pos_nodes.add(curpos_node) 720 | 721 | self.addNode(curpos_node, self.rt_root) 722 | 723 | 724 | old_root = self.rt_root 725 | require_prop = self.changeRoot(curpos_node) 726 | if not self.detect_obs_move: 727 | self.merge_branch(old_root, curpos_node) 728 | 729 | 730 | self.visited_set.clear() 731 | self.rewireRoot.clear() 732 | self.pushedToRewireRoot.clear() 733 | 734 | if exactly_on_root: 735 | print("happen to be on root") 736 | # delete added dummy root 737 | self.tree.remove_edge(curpos_node, old_root) 738 | self.safe_remove_node(curpos_node) 739 | self.rt_root = old_root 740 | elif exactly_on_nxt: 741 | print("happen to be on next_wp") 742 | # delete added dummy root 743 | self.tree.remove_edge(curpos_node, old_root) 744 | self.safe_remove_node(curpos_node) 745 | self.rt_root = old_root 746 | 747 | return require_prop 748 | 749 | def change_labels(self, reg_name): 750 | for node in self.tree.nodes: 751 | if isWithinAABB(node.pos, self.workspace.regions[reg_name]): 752 | node.label = getLabel(node.pos, self.workspace, is_hybrid=self.is_hybrid) 753 | 754 | def codeRestart(self): 755 | # disable current plan 756 | self.cur_target = None 757 | self.goalFound = False 758 | self.cur_path = [] 759 | self.next_wp = None 760 | self.dist_root2next = 0 761 | self.possible_paths = [] 762 | 763 | self.check_all_nodes_to_goal() 764 | 765 | # caution: must assure that the cost is not inf 766 | def check_all_nodes_to_goal(self, is_hybrid = False): 767 | if self.goalFound: 768 | return 769 | 770 | # check rt_root first 771 | goal_updated = self.checkNode2Goal(self.rt_root, is_hybrid, approx=True) 772 | if goal_updated: 773 | print('Already fulfill the task!') 774 | self.updateBestPath() 775 | return 776 | 777 | for node in list(self.tree.nodes()): 778 | 779 | goal_updated = self.checkNode2Goal(node, is_hybrid, approx=True) 780 | if goal_updated: 781 | print('got shortcut') 782 | self.updateBestPath() 783 | break 784 | 785 | # NOTE: invalid_set is deleted 786 | def blockNodes(self): 787 | move_obs_AABB = self.old_dynobs_AABB 788 | for child in list(self.tree.nodes()): 789 | if child == self.rt_root: 790 | continue 791 | 792 | pred_ls = list(self.tree.pred[child].keys()) 793 | if len(pred_ls) ==0: 794 | continue 795 | parent = pred_ls[0] 796 | 797 | # make move_obs_bdr an AABB, 798 | is_intersect = LinesegIntersectsAABB(move_obs_AABB, LineSegment(parent.pos, child.pos)) 799 | if is_intersect: 800 | if isWithinAABB(parent.pos, move_obs_AABB): 801 | # in case parent is root 802 | if parent.cost2Root !=0: 803 | parent.cost2Root = np.inf 804 | 805 | child.cost2Root = np.inf 806 | 807 | 808 | def handle_perception(self, reg_name): 809 | obs_no_affect = False 810 | 811 | if self.detect_obs_move: 812 | self.blockNodes() 813 | 814 | resume_path = self.validate_update_sols() 815 | if not resume_path: 816 | self.sudden_stop() 817 | self.root_just_changed = True 818 | else: 819 | obs_no_affect = True 820 | 821 | # if resume_path is false, then self.codeRestart() is already called in self.validate_update_sols() 822 | resume_path = True 823 | # deal with move obs and new observation 824 | if self.detect_TS_change: 825 | 826 | self.change_labels(reg_name) 827 | 828 | self.sudden_stop() 829 | self.root_just_changed = True 830 | 831 | # if cur plan is invalid on automaton, or blocked by obs (redandent checking) 832 | # or (self.cur_target and self.calc_cost2Root(self.cur_target)== np.inf) 833 | resume_path = self.validate_update_sols() 834 | 835 | return obs_no_affect 836 | 837 | def sample(self): 838 | if self.para['bias_sample']: 839 | x_rand = None 840 | rand_num = uniform(0,1) 841 | 842 | reg_num = len(self.workspace.regions) 843 | rand_sect = self.para['reg_sample_ratio'] /reg_num 844 | 845 | 846 | i=0 847 | for reg_name, reg_bbx in self.workspace.regions.iteritems(): 848 | if rand_num < 1- i*rand_sect and rand_num >= 1- (i+1)*rand_sect: 849 | x_rand = randomPointAABB(reg_bbx) 850 | 851 | return x_rand 852 | i +=1 853 | 854 | x_rand = randomPointAABB(self.workspace.ws_AABB) 855 | 856 | return x_rand 857 | 858 | def calc_near_dist(self): 859 | # self.near_dist = 50 860 | # follow the way in RT-RRT* 861 | self.near_dist = np.sqrt(self.workspace.length*self.workspace.width*self.para['k_max']/np.pi / self.tree.number_of_nodes()) 862 | 863 | def nearest_samp(self, x_rand): 864 | # combine old near() and nearest_samp() 865 | min_dis = np.inf 866 | node_nearest = [] 867 | checked_set= set() 868 | cost_dict = {} 869 | rand_label = getLabel(x_rand, self.workspace, self.is_hybrid) 870 | 871 | for node in self.tree.nodes: 872 | if self.isIsolated(node) and node!= self.rt_root: 873 | continue 874 | 875 | x= node.pos 876 | dis = self.evaluator.euclideanDist(x_rand,x) 877 | 878 | # get the nearest: at least one exists 879 | if dis < min_dis: 880 | node_nearest = [node] 881 | min_dis = dis 882 | elif dis == min_dis: # no needs 883 | node_nearest.append(node) 884 | # near() 885 | if dis > self.near_dist: 886 | continue 887 | 888 | if node.pos not in checked_set: 889 | # check obs 890 | no_collision = collisionFreeAABB(node.pos, x_rand, self.workspace, self.old_dynobs_aabb) 891 | safe_label = self.checkCrossLabel(node, x_rand, rand_label) 892 | if not (no_collision and safe_label): 893 | continue 894 | 895 | cost_dict[(x_rand, node.pos)] = self.evaluator.getCost(node, x_rand) 896 | checked_set.add(node.pos) 897 | 898 | # this is why nearest_samp() is useful in simple case 899 | self.rand_near_nbrs.add(node) 900 | 901 | 902 | return node_nearest[0], cost_dict 903 | 904 | 905 | 906 | # naive steer 907 | def steer(self, x_rand, x_nearest): 908 | if self.evaluator.euclideanDist(x_rand, x_nearest) <= self.para['step_size']: 909 | return x_rand 910 | else: 911 | diff = np.subtract(x_rand, x_nearest) 912 | x_new = np.asarray(x_nearest) + self.para['step_size']* diff / np.linalg.norm(diff) 913 | return tuple(x_new) 914 | 915 | def expandAndRewire(self): 916 | self.calc_near_dist() 917 | # min < dist < max 918 | self.near_dist = min(self.near_dist, self.para['max_near_dist']) 919 | self.near_dist = max(self.near_dist, self.para['min_near_dist']) 920 | 921 | x_rand = self.sample() 922 | 923 | require_extend = True 924 | node_nearest, cost_dict = self.nearest_samp(x_rand) 925 | x_nearest = node_nearest.pos 926 | # if sampled at the existed node 927 | if self.evaluator.euclideanDist(x_nearest, x_rand) < 1e-2: 928 | require_extend = False 929 | # NOTE: rand_near_nodes are nodes around x_rand, not x_new! 930 | x_new = self.steer(x_rand, node_nearest.pos) 931 | new_label = getLabel(x_new, self.workspace, self.is_hybrid) 932 | 933 | if ptInObs(x_new, self.workspace): 934 | require_extend = False 935 | 936 | if require_extend and len(self.rand_near_nbrs) < self.para['k_max']: # or self.evaluator.euclideanDist(x_new, node_nearest) > self.sparse_r: 937 | # do rrt* things 938 | 939 | added_nodes = set() 940 | not_added_nodes = set() 941 | for b_state in self.automaton.nodes: 942 | 943 | # NOTE: In prefix, this job is done by checkNode2Goal 944 | if b_state in self.accept: 945 | continue 946 | # think about until expression 947 | if b_state in self.unreachable_b_states: 948 | continue 949 | 950 | new_node_id = self.added_nodes_cnt 951 | new_node = self.Node(new_node_id, x_new, np.inf, b_state,new_label) 952 | 953 | added = self.extend(new_node, self.rand_near_nbrs, cost_dict) 954 | if added: 955 | 956 | added_nodes.add(new_node) 957 | self.rewireRand.add(new_node) 958 | 959 | # check reach 960 | goal_updated = self.checkNode2Goal(new_node,self.is_hybrid) 961 | if goal_updated: 962 | print('find better goal') 963 | else: 964 | not_added_nodes.add(new_node) 965 | 966 | # add nodes from not_added_nodes to G 967 | for nd in not_added_nodes: 968 | nd.node_id= self.added_nodes_cnt 969 | self.addNode(nd) 970 | added_nodes.add(nd) 971 | 972 | for nd in added_nodes: 973 | nd.same_pos_nodes = added_nodes 974 | 975 | else: 976 | self.rewireRand.add(node_nearest) 977 | for nd in node_nearest.same_pos_nodes: 978 | if nd != node_nearest and self.calc_cost2Root(nd)!=np.inf: 979 | self.rewireRand.add(nd) 980 | 981 | self.rewireRandNode() 982 | 983 | self.rewireFromRoot() 984 | 985 | return x_rand 986 | 987 | 988 | def rewireRandNode(self): 989 | extendRewireRand = set() 990 | while len(self.rewireRand): 991 | Xr = self.rewireRand.pop() 992 | # NOTE: rewireRand is never cleared, nodes may be deleted by merge_branch 993 | if Xr.cost2Root == np.inf: 994 | continue 995 | 996 | near_nodes, cost_dict = self.near_succ(Xr) 997 | rewired_nodes = self.rewire(Xr, near_nodes, cost_dict) 998 | extendRewireRand = extendRewireRand.union(rewired_nodes) 999 | 1000 | idx = 0 1001 | while len(extendRewireRand) and idx<3: #self.allowRewiring(0.5*self.para['time_for_rewire']): # 1002 | idx +=1 1003 | Xr = extendRewireRand.pop() 1004 | # NOTE: rewireRand is never cleared, nodes may be deleted by merge_branch 1005 | if Xr.cost2Root == np.inf: 1006 | continue 1007 | 1008 | near_nodes, cost_dict = self.near_succ(Xr) 1009 | rewired_nodes = self.rewire(Xr, near_nodes, cost_dict) 1010 | extendRewireRand = extendRewireRand.union(rewired_nodes) 1011 | 1012 | 1013 | def allowRewiring(self, threth): 1014 | cur_time = rospy.Time.now() 1015 | if (cur_time- self.timeKeeper).to_sec() < threth: 1016 | return True 1017 | else: 1018 | return False 1019 | 1020 | 1021 | def rewireFromRoot(self): 1022 | if len(self.rewireRoot)== 0: 1023 | self.rewireRoot.add(self.rt_root) 1024 | 1025 | idx = 0 1026 | while len(self.rewireRoot) and self.allowRewiring(self.para['time_for_rewire']): # idx <=5: # 1027 | idx +=1 1028 | Xs = self.rewireRoot.pop() 1029 | # deleted 1030 | if Xs.visited == True: 1031 | continue 1032 | 1033 | near_nodes, cost_dict = self.near_succ(Xs) 1034 | # delete the parent node of Xs 1035 | if len(list(self.tree.pred[Xs].keys())): 1036 | Xs_parent = list(self.tree.pred[Xs].keys())[0] 1037 | if Xs_parent in near_nodes: 1038 | near_nodes.remove(Xs_parent) 1039 | obs_check_dict = None 1040 | rewired_nodes = self.rewire(Xs, near_nodes, cost_dict) 1041 | # add nodes which are not pushed into rewireRoot in current iter 1042 | self.rewireRoot = self.rewireRoot.union(rewired_nodes.difference(self.pushedToRewireRoot)) 1043 | self.pushedToRewireRoot = self.pushedToRewireRoot.union(rewired_nodes) 1044 | 1045 | def handle_self_state(self): 1046 | if len(self.cur_path) ==1 and self.evaluator.euclideanDist(self.rbt_pos, np.array(self.rt_root.pos))< self.para['goal_radius']: 1047 | self.task_initialized = False 1048 | print('TASK COMPLETE!') 1049 | 1050 | return True 1051 | 1052 | if self.seg_num >=2: 1053 | updated_root_id= -1 1054 | for i in range(1, self.seg_num): 1055 | 1056 | pos_to_arrive = self.cur_path[i].pos 1057 | if self.evaluator.euclideanDist(self.rbt_pos, np.array(pos_to_arrive))< self.para['changeRoot_radius']: 1058 | updated_root_id = i 1059 | break 1060 | 1061 | if updated_root_id >1: 1062 | print('Fast forward motion takes over!') 1063 | 1064 | # should change root 1065 | if updated_root_id> 0: 1066 | for j in range(0, updated_root_id): 1067 | self.cur_path.pop(0) #actually rt_root 1068 | new_root = self.cur_path[0] 1069 | 1070 | old_root = self.rt_root 1071 | require_prop = self.changeRoot(new_root) 1072 | if require_prop: 1073 | self.merge_branch(old_root, new_root) 1074 | 1075 | self.root_just_changed = True 1076 | 1077 | resume_path = self.validate_update_sols() 1078 | if not resume_path: 1079 | print('current path invalid') 1080 | 1081 | self.visited_set.clear() 1082 | self.rewireRoot.clear() 1083 | self.pushedToRewireRoot.clear() 1084 | 1085 | return False 1086 | 1087 | def plan_iteration(self, rbt_pos, rviz_mgr): 1088 | finished = False 1089 | 1090 | # in case self.detect_obs_move is set true during iteration. 1091 | # if obs_to_proc == 1, then set self.detect_obs_move as 0. 1092 | obs_to_proc = self.detect_obs_move 1093 | 1094 | self.rbt_pos = rbt_pos 1095 | if not self.task_initialized: 1096 | # rviz_mgr.clear_all() 1097 | return False, False 1098 | 1099 | self.dist_root2rbt = self.evaluator.euclideanDist(self.rbt_pos, np.array(self.rt_root.pos)) 1100 | 1101 | if self.next_wp is not None: 1102 | self.dist_next2rbt = self.evaluator.euclideanDist(self.rbt_pos, np.array(self.next_wp.pos)) 1103 | 1104 | if len(self.changed_regs): 1105 | reg = self.changed_regs.pop() 1106 | else: 1107 | reg = None 1108 | obs_no_affect = self.handle_perception(reg) 1109 | 1110 | if not self.root_just_changed and self.goalFound: 1111 | finished = self.handle_self_state() 1112 | 1113 | self.timeKeeper = rospy.Time.now() 1114 | x_rand = self.expandAndRewire() 1115 | 1116 | if len(self.possible_paths): 1117 | self.validate_update_sols() 1118 | 1119 | # post process 1120 | self.get_bad_regs() 1121 | 1122 | if rviz_mgr: 1123 | rviz_mgr.populateRviz() 1124 | 1125 | rviz_mgr.draw_rtroot(self.rt_root) 1126 | edges_list = list(self.tree.edges()) 1127 | rviz_mgr.display_edges(edges_list) 1128 | nd_ls = list(self.tree.nodes()) 1129 | leaf_ls = [nd for nd in nd_ls if self.tree.out_degree(nd) == 0] 1130 | rviz_mgr.draw_leaves(leaf_ls) 1131 | rviz_mgr.draw_empty() 1132 | # rviz_mgr.draw_rewired_root(self.pushedToRewireRoot) 1133 | rviz_mgr.drawFinalPath(self.cur_path) 1134 | 1135 | 1136 | self.rand_near_nbrs.clear() 1137 | self.detect_goal_changed = False 1138 | if obs_to_proc: 1139 | self.detect_obs_move = False 1140 | self.detect_TS_change = (len(self.changed_regs) != 0) 1141 | self.root_just_changed = False 1142 | 1143 | 1144 | return finished, obs_no_affect 1145 | 1146 | 1147 | def unify_automata(self, automaton, aut_nodes_dict): 1148 | 1149 | 1150 | 1151 | renamed_graph = automaton.copy() 1152 | renamed_graph = nx.relabel_nodes(renamed_graph, aut_nodes_dict) 1153 | 1154 | initial_set = set() 1155 | for nd in renamed_graph.graph['initial']: 1156 | initial_set.add(aut_nodes_dict[nd]) 1157 | renamed_graph.graph['initial'] = list(initial_set) 1158 | 1159 | accept_set = set() 1160 | for nd in renamed_graph.graph['accept']: 1161 | accept_set.add(aut_nodes_dict[nd]) 1162 | renamed_graph.graph['accept'] = list(accept_set) 1163 | 1164 | return renamed_graph 1165 | 1166 | def reset_task(self, new_buchi): 1167 | aut_dict = get_automaton_dict(new_buchi) 1168 | self.automaton = self.unify_automata(new_buchi, aut_dict) 1169 | self.accept = self.automaton.graph['accept'] 1170 | self.unreachable_b_states = self.preprocessDFA() 1171 | 1172 | self.possible_paths = [] 1173 | self.detect_TS_change = False 1174 | self.bad_regs = set() 1175 | self.dist_root2next = 0 1176 | self.dist_root2rbt = 0 1177 | self.dist_next2rbt = 0 1178 | 1179 | self.cur_target = None 1180 | self.cur_path = [] 1181 | self.rand_near_nbrs = set() 1182 | self.rewireRand = set() 1183 | self.visited_set = set() 1184 | self.rewireRoot = set() 1185 | self.pushedToRewireRoot = set() # has been pushed to rewireRoot within current iter 1186 | self.detect_goal_changed = False 1187 | self.detect_obs_move = False 1188 | self.invalid_set = set() 1189 | self.next_wp = None 1190 | 1191 | self.goalFound = False 1192 | self.nodesAroundGoal = [] 1193 | 1194 | self.task_initialized = True 1195 | 1196 | 1197 | def wrap_plan(self): 1198 | plan_skeleton = '' 1199 | for nd in self.cur_path: 1200 | node_text = '-> ' 1201 | node_text += '({:.2f},{:.2f}) '.format(nd.pos[0], nd.pos[1]) 1202 | if len(nd.label): 1203 | node_text+= '['+ list(nd.label)[0] +']' 1204 | plan_skeleton += node_text 1205 | 1206 | return plan_skeleton 1207 | 1208 | 1209 | def supplement_nodes(self): 1210 | # in case 1st time plan 1211 | if len(self.tree.nodes)<=1: 1212 | return 1213 | 1214 | new_b_set = set(self.automaton.nodes()) 1215 | new_b_set = new_b_set.difference(self.accept) 1216 | for pos, node in self.nodes_pos_dict.items(): 1217 | existed_b_list = [nd.b_state for nd in node.same_pos_nodes] 1218 | existed_b_set = set(existed_b_list) 1219 | lack_b_set = new_b_set.difference(existed_b_set) 1220 | 1221 | # add lost nodes 1222 | for b_state in lack_b_set: 1223 | lack_id = self.added_nodes_cnt 1224 | lack_node = self.Node(lack_id, pos, np.inf, b_state, node.label) 1225 | self.tree.add_node(lack_node) 1226 | self.added_nodes_cnt +=1 1227 | 1228 | node.same_pos_nodes.add(lack_node) 1229 | lack_node.same_pos_nodes = node.same_pos_nodes # addr of a list 1230 | 1231 | def reset_tree(self, rviz_mgr): 1232 | # in case 1st time plan 1233 | if len(self.tree.nodes)<=1: 1234 | return 1235 | 1236 | if len(self.automaton.graph['initial'])!=1: 1237 | raise NotImplementedError('more initials') 1238 | 1239 | # dummy_init 1240 | dummy_id = self.added_nodes_cnt 1241 | dummy_label = getLabel(self.rt_root.pos, self.workspace) 1242 | init_b_state = self.automaton.graph['initial'][0] 1243 | dummy_node = self.Node(dummy_id, self.rt_root.pos, 0, init_b_state, dummy_label) 1244 | dummy_node.same_pos_nodes.add(dummy_node) 1245 | self.addNode(dummy_node, self.rt_root) 1246 | self.added_nodes_cnt +=1 1247 | 1248 | old_root = self.rt_root 1249 | require_prop = self.changeRoot(dummy_node) 1250 | self.merge_branch(old_root, dummy_node) 1251 | 1252 | # delete added dummy root 1253 | self.tree.remove_edge(dummy_node, old_root) 1254 | self.safe_remove_node(dummy_node) 1255 | self.rt_root = old_root 1256 | -------------------------------------------------------------------------------- /LTLMP/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JINXER000/multiple-task-temporal-logic-planning/16bc21cac81b733efd0e657791ac492d2f0696fc/LTLMP/src/__init__.py -------------------------------------------------------------------------------- /LTLMP/src/formula_parser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JINXER000/multiple-task-temporal-logic-planning/16bc21cac81b733efd0e657791ac492d2f0696fc/LTLMP/src/formula_parser/__init__.py -------------------------------------------------------------------------------- /LTLMP/src/formula_parser/lexer.py: -------------------------------------------------------------------------------- 1 | import ply.lex as lex 2 | 3 | tokens = ( 4 | "SYMBOL", 5 | "AND", "OR", 6 | "NOT", 7 | "TRUE", 8 | "LPAREN", "RPAREN") 9 | 10 | t_SYMBOL = r"[a-z]+[a-z0-9_]*" 11 | t_TRUE = r"1" 12 | t_AND = r"&&" 13 | t_OR = r"\|\|" 14 | t_NOT = r"!" 15 | t_LPAREN = r"\(" 16 | t_RPAREN = r"\)" 17 | 18 | t_ignore = " " 19 | 20 | def t_error(t): 21 | print "Illegal character '%s'" % t.value[0] 22 | 23 | def get_lexer(): 24 | return lex.lex() 25 | -------------------------------------------------------------------------------- /LTLMP/src/formula_parser/ltlf2dfa.py: -------------------------------------------------------------------------------- 1 | #!/home/joseph/yzchen_ws/env/py3torch/bin/python3 2 | #/home/joseph/yzchen_ws/task_planning/ltl_ros_ws/src/src/P_MAS_TG/rrt_collection/formula_parser/ltlf2dfa.py 3 | import sys 4 | sys.path.append('/home/joseph/yzchen_ws/env/LTLf2DFA/') 5 | sys.path.append('/home/joseph/yzchen_ws/env/LTLf2DFA/ltlf2dfa/') 6 | from ltlf2dfa.parser.ltlf import LTLfParser 7 | 8 | 9 | if __name__ == '__main__': 10 | parser = LTLfParser() 11 | # formula_str = "F (pond && F grassland) && FG base" 12 | # formula_str = "(!grassland U pond) && F grassland " 13 | formula_str = "F (pond && F grassland)" 14 | formula = parser(formula_str) 15 | 16 | # dfa = formula.to_dfa() 17 | 18 | # path = "until.gv" 19 | # fp= open(path, "w") 20 | # test_gv = fp.write(dfa) 21 | # fp.close() -------------------------------------------------------------------------------- /LTLMP/src/formula_parser/ltlf_parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | import networkx as nx 3 | from networkx.classes.digraph import DiGraph 4 | 5 | from parser import parse as parse_guard 6 | 7 | 8 | 9 | def find_symbols(formula): 10 | regex = re.compile(r"[a-z]+[a-z0-9]*") 11 | matches = regex.findall(formula) 12 | symbols = list() 13 | for match in matches: 14 | symbols += [match] 15 | symbols = list(set(symbols)) 16 | symbols.sort() 17 | return symbols 18 | 19 | 20 | def find_states(edges, dot_text): 21 | initial = set() 22 | accept = set() 23 | for (f,t) in edges.keys(): 24 | if f == 'init': 25 | initial.add(t) 26 | 27 | research = re.search(r'doublecircle];\s(\d+)', dot_text) 28 | accept.add(research.group(1)) 29 | 30 | return (list(initial), list(accept)) 31 | 32 | def getDFA(path, formula): 33 | fp= open(path) 34 | test_gv = fp.read() 35 | fp.close() 36 | 37 | G = DiGraph(nx.nx_pydot.read_dot(path)) 38 | 39 | initial, accept = find_states(G.edges, test_gv) 40 | symbols = find_symbols(formula) 41 | 42 | DFA = DiGraph(initial=initial, accept=accept, symbols=symbols) 43 | 44 | # G.graph['initial'] = initial 45 | # G.graph['accept'] = accept 46 | for nd in G.nodes: 47 | if nd == 'init': 48 | continue 49 | DFA.add_node(nd) 50 | 51 | for (ef,et) in G.edges.keys(): 52 | if ef == 'init': 53 | continue 54 | guard_formula = G.edges[(ef,et)]['label'].strip('"') 55 | guard_formula = guard_formula.replace('~', '!') 56 | guard_formula = guard_formula.replace('&', '&&') 57 | guard_formula = guard_formula.replace('|', '||') 58 | guard_formula = guard_formula.replace('true', '1') 59 | 60 | guard_expr = parse_guard(guard_formula) 61 | # G[ef][et]["guard"] = guard_expr 62 | DFA.add_edge(ef, et, guard=guard_expr, guard_formula=guard_formula) 63 | 64 | 65 | return DFA 66 | 67 | import os 68 | import subprocess 69 | root_dir = r'/home/dyl/yzchen_ws/task_ws/hmi_misc/' 70 | ltl_path = os.path.join(root_dir, 'output_ltl.txt') 71 | buchi_path = os.path.join(root_dir, 'rqt_dfa.gv') 72 | 73 | def call_LTLf2DFA(formula): 74 | 75 | with open(ltl_path, 'w') as fp: 76 | fp.write(formula) 77 | # translate to DFA 78 | ltl2aut_cmd = 'python3 /home/dyl/yzchen_ws/task_ws/LTLf2DFA/ltlf2dfa.py' 79 | process = subprocess.Popen(ltl2aut_cmd.split(), stdout=subprocess.PIPE) 80 | output, error = process.communicate() 81 | 82 | # directly read gv 83 | DFA = getDFA(buchi_path, formula) 84 | 85 | return DFA 86 | 87 | if __name__ == '__main__': 88 | path = "mona.gv" 89 | formula = '<> (pond && <> grassland) && <>[] pond' 90 | dfa = getDFA(path, formula) 91 | 92 | # regex = re.match(r"doublecircle];\s([0-9])", test_gv) 93 | 94 | # research = re.search(r'doublecircle];\s(\d+)', test_gv) 95 | 96 | 97 | 98 | 99 | 100 | print('TODO: get accept') 101 | -------------------------------------------------------------------------------- /LTLMP/src/formula_parser/parser.py: -------------------------------------------------------------------------------- 1 | from lexer import get_lexer 2 | import itertools 3 | 4 | 5 | class Expression(object): 6 | name = "Expression" 7 | def __iter__(self): 8 | raise NotImplementedError() 9 | 10 | def check(self, label): 11 | raise NotImplementedError() 12 | 13 | def distance(self, label): 14 | raise NotImplementedError() 15 | 16 | def nnf(self): 17 | return self 18 | 19 | class SymbolExpression(Expression): 20 | def __init__(self, name): 21 | self.name = name 22 | self.symbol = name 23 | 24 | def __repr__(self): 25 | return "SymbolExpression(%s)" % str(self.symbol) 26 | 27 | def __iter__(self): 28 | for expr in [self]: 29 | yield expr 30 | 31 | def children(self): 32 | return [] 33 | 34 | def check(self, label): 35 | return self.symbol in label 36 | 37 | def distance(self, label): 38 | if self.symbol in label: 39 | return 0 40 | else: 41 | return 1 42 | 43 | class NotSymbolExpression(Expression): 44 | def __init__(self, name): 45 | self.name = "!%s" % name 46 | self.symbol = name 47 | 48 | def __repr__(self): 49 | return "NotSymbolExpression(%s)" % str(self.symbol) 50 | 51 | def __iter__(self): 52 | for expr in [self]: 53 | yield expr 54 | 55 | def children(self): 56 | return [] 57 | 58 | def check(self, label): 59 | return self.symbol not in label 60 | 61 | def distance(self, label): 62 | if self.symbol not in label: 63 | return 0 64 | else: 65 | return 1 66 | 67 | class TrueExpression(Expression): 68 | name = "TRUE" 69 | def __init__(self): 70 | pass 71 | 72 | def __repr__(self): 73 | return "TrueExpression()" 74 | 75 | def __iter__(self): 76 | for expr in [self]: 77 | yield expr 78 | 79 | def children(self): 80 | return [] 81 | 82 | def check(self, label): 83 | return True 84 | 85 | def distance(self, label): 86 | return 0 87 | 88 | class NotExpression(Expression): 89 | name = "NOT" 90 | def __init__(self, inner): 91 | self.inner = inner 92 | 93 | def __repr__(self): 94 | return "NotExpression(%s)" % str(self.inner) 95 | 96 | def __iter__(self): 97 | for expr in itertools.chain([self], self.inner): 98 | yield expr 99 | 100 | def children(self): 101 | return [self.inner] 102 | 103 | def check(self, label): 104 | return not self.inner.check(label) 105 | 106 | def nnf(self): 107 | if isinstance(self.inner, SymbolExpression): 108 | s = NotSymbolExpression(self.inner.name) 109 | return s 110 | elif isinstance(self.inner, ORExpression): 111 | left = NotExpression(self.inner.left).nnf() 112 | right = NotExpression(self.inner.right).nnf() 113 | s = ANDExpression(left, right) 114 | return s 115 | elif isinstance(self.inner, ANDExpression): 116 | left = NotExpression(self.inner.left).nnf() 117 | right = NotExpression(self.inner.right).nnf() 118 | s = ORExpression(left, right) 119 | return s 120 | # revised by joseph 121 | elif isinstance(self.inner, NotExpression): 122 | s = self.inner.inner 123 | return s 124 | raise Exception("Unexpected child of NotExpression") 125 | 126 | class BinExpression(Expression): 127 | def __init__(self, left, right): 128 | self.left = left 129 | self.right = right 130 | 131 | def __iter__(self): 132 | for expr in itertools.chain([self], self.left, self.right): 133 | yield expr 134 | 135 | def children(self): 136 | return [self.left, self.right] 137 | 138 | def nnf(self): 139 | self.left = self.left.nnf() 140 | self.right = self.right.nnf() 141 | return self 142 | 143 | class ORExpression(BinExpression): 144 | name = "OR" 145 | def __repr__(self): 146 | return "ORExpression(%s, %s)" % (str(self.left), str(self.right)) 147 | 148 | def check(self, label): 149 | return self.left.check(label) or self.right.check(label) 150 | 151 | def distance(self, label): 152 | ldist = self.left.distance(label) 153 | rdist = self.right.distance(label) 154 | return min([ldist, rdist]) 155 | 156 | class ANDExpression(BinExpression): 157 | name = "AND" 158 | def __repr__(self): 159 | return "ANDExpression(%s, %s)" % (str(self.left), str(self.right)) 160 | 161 | def check(self, label): 162 | return self.left.check(label) and self.right.check(label) 163 | 164 | def distance(self, label): 165 | return self.left.distance(label) + self.right.distance(label) 166 | 167 | class Parser(object): 168 | def __init__(self, formula): 169 | lexer = get_lexer() 170 | lexer.input(formula) 171 | self.formula = formula 172 | self.tokens = list(lexer) 173 | 174 | def symbols(self): 175 | syms = list() 176 | for token in self.tokens: 177 | if token.type == "SYMBOL": 178 | syms += [token.value] 179 | return list(set(syms)) 180 | 181 | def parse(self): 182 | expr = self.orx() # recursively construct syntax tree: or; and; not 183 | expr = expr.nnf() # transform the tree to negative normal form 184 | expr.formula = self.formula 185 | return expr 186 | 187 | def orx(self): 188 | lhs = self.andx() 189 | if len(self.tokens) == 0 or self.tokens[0].type == "RPAREN": 190 | return lhs 191 | elif self.tokens[0].type == "OR": 192 | self.tokens.pop(0) 193 | rhs = self.andx() 194 | lhs = ORExpression(lhs, rhs) 195 | while len(self.tokens) > 0 and self.tokens[0].type == "OR": 196 | self.tokens.pop(0) 197 | rhs = self.andx() 198 | lhs = ORExpression(lhs, rhs) 199 | return lhs 200 | else: 201 | raise Exception("Expected OR, RPAREN or nothing but got %s" % self.tokens[0]) 202 | 203 | def andx(self): 204 | lhs = self.notx() 205 | if len(self.tokens) == 0 or self.tokens[0].type in ["OR", "RPAREN"]: 206 | return lhs 207 | elif self.tokens[0].type == "AND": 208 | self.tokens.pop(0) 209 | rhs = self.notx() 210 | lhs = ANDExpression(lhs, rhs) 211 | while len(self.tokens) > 0 and self.tokens[0].type == "AND": 212 | self.tokens.pop(0) 213 | rhs = self.notx() 214 | lhs = ANDExpression(lhs, rhs) 215 | return lhs 216 | else: 217 | raise Exception("Expected OR, AND or nothing but got %s" % self.tokens[0]) 218 | 219 | def notx(self): 220 | if self.tokens[0].type == "NOT": 221 | self.tokens.pop(0) 222 | return NotExpression(self.parx()) 223 | else: 224 | return self.parx() 225 | 226 | def parx(self): 227 | if self.tokens[0].type == "LPAREN": 228 | self.tokens.pop(0) 229 | expr = self.orx() 230 | if self.tokens[0].type != "RPAREN": 231 | raise Exception("Expected RPAREN but got %s" % self.tokens[0]) 232 | self.tokens.pop(0) 233 | elif self.tokens[0].type == "SYMBOL": 234 | expr = SymbolExpression(self.tokens[0].value) 235 | self.tokens.pop(0) 236 | elif self.tokens[0].type == "TRUE": 237 | expr = TrueExpression() 238 | self.tokens.pop(0) 239 | else: 240 | raise Exception("Expected LPAREN or SYMBOL but got %s" % self.tokens[0]) 241 | return expr 242 | 243 | def parse(formula): 244 | parser = Parser(formula) 245 | return parser.parse() 246 | 247 | if __name__ == '__main__': 248 | guard_expr = parse('!(a || b) && c') 249 | # guard_expr = parse('!a || b') 250 | # syntax tree: or; and; not; paranthes; or; and; not; symbol 251 | 252 | # NOTE: formula itself won't be transformed to nnf 253 | -------------------------------------------------------------------------------- /LTLMP/src/gazebo_HMI_plugin.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from utils.workspace import Workspace3D 3 | from utils.misc_func import getLabel 4 | from RTLTL import RT_LTL_planner 5 | from utils.naive_evaluator import evaluator 6 | from formula_parser.ltlf_parser import getDFA 7 | from ros_gazebo_HMI_base import gazebo_HMI_base, DiscreteMode 8 | 9 | import rospy 10 | 11 | import os 12 | 13 | 14 | root_dir = r'/home/joseph/yzchen_ws/task_planning/ltl_translate/ltl-amdp/language/models/torch/rqt_IO' 15 | buchi_path = os.path.join(root_dir, 'dfa_remote.gv') 16 | 17 | class motionA2B_HMI_RTMQ_ros(gazebo_HMI_base): 18 | def __init__(self, workspace, para): 19 | 20 | super(motionA2B_HMI_RTMQ_ros, self).__init__(workspace, para) 21 | 22 | 23 | def translate_LTL(self, formula): 24 | # invalid input (get from rosparam) 25 | if formula == 0 or formula[0] == '0': 26 | return 0 27 | 28 | DFA = getDFA(buchi_path, formula) 29 | self.pub_msg_rqt('Automata constructed', self.exeInfo_pub) 30 | return DFA 31 | 32 | def on_initialize(self, init_pos, workspace): 33 | init_label = getLabel(init_pos, workspace) 34 | 35 | root = RT_LTL_planner.Node(0, init_pos, 0, self.automaton.graph['initial'][0], init_label) 36 | root.same_pos_nodes.add(root) 37 | dist_eval = evaluator() 38 | self.LTLMP = RT_LTL_planner(root, None, workspace, dist_eval, self.para, self.automaton, 'prefix') 39 | 40 | rospy.Timer(rospy.Duration(1), self.checkShortCut) 41 | 42 | 43 | def on_update(self): 44 | # in case during planning, the tree is reset by rqt_monitor() 45 | self.mutex.acquire() 46 | 47 | if self.mode == DiscreteMode.planning and self.LTLMP.goalFound: 48 | self.mode = DiscreteMode.executing 49 | initplan_time = (rospy.Time.now() - self.initplan_start_time).to_sec() 50 | print('initplan_time is ' + str(initplan_time)) 51 | self.pub_msg_rqt('initplan_time is ' + str(initplan_time) + 's', self.exeInfo_pub) 52 | 53 | plan_skeleton = self.LTLMP.wrap_plan() 54 | self.pub_msg_rqt(plan_skeleton, self.trace_pub) 55 | 56 | ready_obs = False 57 | if self.LTLMP.detect_obs_move: 58 | ready_obs = True 59 | 60 | ready_TS = False 61 | if self.LTLMP.detect_TS_change: 62 | ready_TS = True 63 | 64 | finished, obs_no_affect = self.LTLMP.plan_iteration(self.rbt_pos, self.rviz_mgr) 65 | 66 | # if need replan 67 | if ((ready_obs and obs_no_affect == False) or ready_TS) and self.doing_replan == False: 68 | self.doing_replan = True 69 | self.replan_start_time = rospy.Time.now() 70 | 71 | log_text = "Prior knowledge incorrect, replanning!" 72 | if not ready_TS: 73 | log_text = "Blocked by obstacles, replanning! " 74 | self.pub_msg_rqt(log_text, self.exeInfo_pub) 75 | 76 | # if the replan succeed 77 | if self.doing_replan == True and self.LTLMP.goalFound == True and obs_no_affect == False: 78 | self.doing_replan = False 79 | self.replan_total_times += 1 80 | replan_time = (rospy.Time.now() - self.replan_start_time).to_sec() 81 | self.replan_total_secs += replan_time 82 | print('replan using ' + str(replan_time) + ' s') 83 | self.pub_msg_rqt('replan using ' + str(replan_time) + 's', self.exeInfo_pub) 84 | 85 | plan_skeleton = self.LTLMP.wrap_plan() 86 | self.pub_msg_rqt(plan_skeleton, self.trace_pub) 87 | 88 | if finished: 89 | finish_time = rospy.Time.now() 90 | total_time = (finish_time - self.start_time).to_sec() 91 | print('elapse time is ' + str(total_time)) 92 | if self.replan_total_times > 0: 93 | print('avg replan is ' + str(self.replan_total_secs / self.replan_total_times)) 94 | 95 | self.mode = DiscreteMode.notInit 96 | self.pub_msg_rqt('elapse time is ' + str(total_time) + 's', self.exeInfo_pub) 97 | 98 | if self.LTLMP.goalFound: 99 | ## use astar 100 | path_seg = [] 101 | # # if cur_pose is not the 1st wp 102 | for nd in self.LTLMP.cur_path: 103 | path_seg.append(nd.pos) 104 | 105 | if len(self.LTLMP.rt_root.label): 106 | if len(nd.label) and list(nd.label)[0] != list(self.LTLMP.rt_root.label)[0]: 107 | break 108 | else: 109 | if len(nd.label): 110 | break 111 | # for fast forward motion 112 | self.LTLMP.seg_num = len(path_seg) 113 | if len(path_seg): 114 | self.pub_temp_goal(path_seg[-1]) 115 | 116 | self.mutex.release() 117 | 118 | 119 | def on_reset_task(self): 120 | # reset the planner 121 | self.LTLMP.reset_task(self.automaton) 122 | self.LTLMP.supplement_nodes() 123 | self.LTLMP.reset_tree(self.rviz_mgr) 124 | 125 | def checkShortCut(self, event): 126 | if not self.doing_replan and self.mode != DiscreteMode.planning: 127 | return 128 | self.LTLMP.check_all_nodes_to_goal() 129 | 130 | 131 | if __name__ == '__main__': 132 | # temp for test 133 | rospy.set_param('workspace_choice', 'Vicon room') 134 | 135 | workspace = Workspace3D() 136 | 137 | para = dict() 138 | para['step_size'] = np.inf 139 | para['real_time_plot'] = False 140 | para['goal_radius'] = 1 141 | para['time_for_rewire'] = 0.2 142 | 143 | # tell if to change root 144 | para['changeRoot_radius'] = 0.4 145 | # same solution radius 146 | para['sameSol_radius'] = 2 147 | # region bias sampling 148 | para['reg_sample_ratio'] = 0.24 149 | # near dist 150 | para['min_near_dist'] = 3 151 | para['max_near_dist'] = 5 152 | para['k_max'] = 100 153 | 154 | para['bias_sample'] = True 155 | 156 | para['fov_dist'] = 2 157 | para['reg_change_ls'] = ['l1', 'l3'] 158 | 159 | node = motionA2B_HMI_RTMQ_ros(workspace, para) 160 | 161 | -------------------------------------------------------------------------------- /LTLMP/src/ros_gazebo_HMI_base.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | 4 | from utils.misc_func import genWorkspacePntCld, fovSense, setup_pybullet 5 | from utils.tree_visualizer import rviz_maker 6 | import threading 7 | 8 | import rospy 9 | from geometry_msgs.msg import PoseStamped, PoseWithCovarianceStamped 10 | from nav_msgs.msg import Path 11 | from sensor_msgs.msg import PointCloud2 12 | import std_msgs.msg 13 | import sensor_msgs.point_cloud2 as pcl2 14 | from nav_msgs.msg import Odometry 15 | from std_msgs.msg import String 16 | from tf.transformations import quaternion_from_euler 17 | 18 | from ltl3.msg import activity_pose 19 | 20 | from enum import Enum 21 | DiscreteMode = Enum('DiscreteMode', ('notInit', 'planning', 'executing')) 22 | 23 | robot_name = '' 24 | 25 | class gazebo_HMI_base(object): 26 | def __init__(self, workspace, para): 27 | rospy.init_node('HMI_LTL', anonymous=False) 28 | 29 | self.node_init_common(para, workspace) 30 | 31 | self.exeInfo_pub = rospy.Publisher("/HMI/exe_info", String, queue_size=1) 32 | self.trace_pub = rospy.Publisher("/HMI/plan_skeleton", String, queue_size=1) 33 | 34 | # customized initialize 35 | self.MP_initialized = False 36 | # wait until got rbt pose 37 | init_pos = self.rbt_pos 38 | while init_pos is None: 39 | init_pos = self.rbt_pos 40 | 41 | goal = None 42 | self.rviz_mgr = rviz_maker(init_pos, goal, workspace, 1, True) 43 | 44 | self.automaton = None 45 | rospy.Timer(rospy.Duration(1.0), self.rqt_monitor) 46 | rospy.set_param('cur_task', 0) 47 | 48 | setup_pybullet(workspace, use_gui = False) 49 | 50 | # wait for formula 51 | while self.mode == DiscreteMode.notInit or self.automaton == 0: 52 | pass 53 | 54 | 55 | self.on_initialize(init_pos, workspace) 56 | 57 | self.MP_initialized = True 58 | 59 | rospy.Timer(rospy.Duration(0.1), self.rbt_routine) 60 | 61 | self.start_time = rospy.Time.now() 62 | rospy.spin() 63 | 64 | 65 | # MUST IMPLEMENT THIS 66 | def translate_LTL(self, formula): 67 | raise NotImplementedError('LTLf2DFA or LTL2BA?') 68 | 69 | def on_reset_task(self): 70 | raise NotImplementedError('This planner does not support multi-query') 71 | 72 | def rqt_monitor(self, event): 73 | 74 | 75 | if self.mode == DiscreteMode.notInit: 76 | self.mutex.acquire() 77 | 78 | self.rviz_mgr.populateRviz() 79 | formula = rospy.get_param('cur_task') 80 | 81 | automaton = self.translate_LTL(formula) 82 | if automaton != 0: 83 | 84 | self.automaton = automaton 85 | 86 | print ('get task: '+ formula) 87 | # for multi query 88 | if self.MP_initialized: 89 | self.on_reset_task() 90 | 91 | self.start_time = rospy.Time.now() 92 | self.initplan_start_time = rospy.Time.now() 93 | 94 | self.mode = DiscreteMode.planning 95 | rospy.set_param('cur_task', 0) 96 | 97 | self.mutex.release() 98 | else: # abort 99 | pass 100 | 101 | 102 | 103 | def pub_msg_rqt(self, text, puber): 104 | info_msg = String() 105 | info_msg.data = text 106 | puber.publish(info_msg) 107 | 108 | 109 | 110 | 111 | def node_init_common(self, para, workspace): 112 | self.para = para 113 | 114 | self.mutex = threading.Lock() 115 | 116 | self.mode = DiscreteMode.notInit 117 | self.doing_replan = False 118 | self.replan_start_time = None 119 | self.replan_total_secs = 0 120 | self.replan_total_times = 0 121 | self.start_time = rospy.Time.now() 122 | self.initplan_start_time = rospy.Time.now() 123 | 124 | self.pynt_stamp_idx = 0 125 | self.pynt_pub = rospy.Publisher("/forbid_reg_cloud", PointCloud2, queue_size=1) 126 | 127 | self.cmd_header = 0 128 | self.temp_goal_pub = rospy.Publisher(robot_name+"/move_act", activity_pose) 129 | self.path_pub = rospy.Publisher('/cur_path', Path, queue_size=1) 130 | 131 | 132 | # moving obs related 133 | # not for real test! 134 | self.obs_move_pub = rospy.Publisher('/initialpose', PoseWithCovarianceStamped, queue_size=1) 135 | self.obs_initialize() 136 | self.new_goal_sub = rospy.Subscriber('/initialpose', PoseWithCovarianceStamped, self.obs_move_CB, queue_size=1) 137 | self.new_dynamic_obs = workspace.dynamic_obs_center 138 | 139 | # rospy.Timer(rospy.Duration(0.2), self.move_obs_fake) 140 | 141 | self.rbt_pos = None 142 | self.rbt_odom_sub = rospy.Subscriber(robot_name+'/odom', Odometry, self.rbt_odom_CB, queue_size=1) #/iris_0 143 | 144 | 145 | 146 | # MUST IMPLEMENT THIS 147 | def on_initialize(self, init_pos, workspace): 148 | raise NotImplementedError('must implement initialize') 149 | 150 | def rbt_routine(self, event): 151 | # perception 152 | # self.LTLMP.workspace.update_dynamic_obstacle(self.new_dynamic_obs) 153 | self.do_perception() 154 | self.pub_pntcld() 155 | 156 | # planning 157 | self.on_update() 158 | 159 | # MUST IMPLEMENT THIS 160 | def on_update(self): 161 | raise NotImplementedError('must implement update') 162 | 163 | def rbt_odom_CB(self, msg): 164 | # tuples are immutable therefore a = sign does not create a reference but a copy as expected. 165 | 166 | rbt_pos_x = msg.pose.pose.position.x 167 | rbt_pos_y = msg.pose.pose.position.y 168 | rbt_pos_z = msg.pose.pose.position.z 169 | self.rbt_pos = (rbt_pos_x, rbt_pos_y, rbt_pos_z) 170 | 171 | 172 | 173 | # same condition for each planner 174 | def do_perception(self): 175 | reg_name = fovSense(self.rbt_pos, self.para['fov_dist'], self.LTLMP.workspace, self.para['reg_change_ls']) 176 | if reg_name is not None and len(self.rviz_mgr.changed_regs)==0: 177 | 178 | self.LTLMP.workspace.properties[reg_name] = '' 179 | 180 | self.LTLMP.detect_TS_change = True 181 | self.LTLMP.changed_regs.add(reg_name) 182 | self.rviz_mgr.changed_regs.add( reg_name) 183 | 184 | self.obs_detection() 185 | 186 | def obs_detection(self): 187 | sensed_dynamic_obs = self.circle_sense_obs(self.rbt_pos, self.LTLMP.workspace.dynamic_obs_center) 188 | 189 | # sensed_dynamic_obs = sense_obs(self.rbt_pos, self.para['fov_dist'], self.LTLMP.workspace, 'd3') 190 | if sensed_dynamic_obs: 191 | cur_dynobs_pos = self.LTLMP.workspace.dynamic_obs_center 192 | obs_diff = self.LTLMP.evaluator.euclideanDist(self.LTLMP.old_dynamic_obs, cur_dynobs_pos) 193 | if obs_diff>3: 194 | self.LTLMP.detect_obs_move = True 195 | print('OBS move detected') 196 | 197 | self.LTLMP.old_dynamic_obs = cur_dynobs_pos 198 | self.LTLMP.old_dynobs_aabb = self.LTLMP.workspace.dyn_obs['d3'] 199 | 200 | 201 | def obs_initialize(self): 202 | self.dyn_obs_mover_msg = PoseWithCovarianceStamped() 203 | self.obs_move_dir = -1 204 | self.dyn_obs_mover_msg.header.stamp= rospy.Time.now() 205 | self.dyn_obs_mover_msg.header.frame_id = 'map' 206 | self.dyn_obs_mover_msg.pose.pose.position.x = -2.5 207 | self.dyn_obs_mover_msg.pose.pose.position.y = -0.5 208 | self.dyn_obs_mover_msg.pose.pose.position.z = -1 209 | self.obs_move_pub.publish(self.dyn_obs_mover_msg) 210 | 211 | def move_obs_fake(self, event): 212 | move_speed = 0.02 213 | cur_obs_axis = self.dyn_obs_mover_msg.pose.pose.position.x 214 | if cur_obs_axis>3.5: 215 | self.obs_move_dir = -1 216 | elif cur_obs_axis<-1: 217 | self.obs_move_dir = 1 218 | 219 | self.dyn_obs_mover_msg.pose.pose.position.x +=self.obs_move_dir*move_speed 220 | self.obs_move_pub.publish(self.dyn_obs_mover_msg) 221 | 222 | def obs_move_CB(self, msg): # covairance have pose.pose 223 | # self.mutex.acquire() 224 | 225 | new_obs_x= msg.pose.position.x/self.rviz_mgr.scale 226 | new_obs_y = msg.pose.position.y/self.rviz_mgr.scale 227 | 228 | # new_dynamic_obs = (new_obs_x, new_obs_y) 229 | # self.LTLMP.workspace.update_dynamic_obstacle(new_dynamic_obs) 230 | # print('received new dyn obs pos: '+new_dynamic_obs) 231 | 232 | # self.mutex.release() 233 | 234 | 235 | def circle_sense_obs(self, rbt_pos, obs_pos): 236 | obs_dist = self.LTLMP.evaluator.euclideanDist(rbt_pos, obs_pos) 237 | if obs_dist < self.para['fov_dist']: 238 | return True 239 | else: 240 | return False 241 | 242 | 243 | 244 | 245 | def pub_pntcld(self): 246 | # if pynt changes, then 247 | # self.handle_pynt_changes() 248 | 249 | cld_pynts = genWorkspacePntCld(self.LTLMP.workspace, self.LTLMP.bad_regs, self.LTLMP.old_dynobs_aabb) 250 | 251 | 252 | #header 253 | header = std_msgs.msg.Header() 254 | header.stamp = rospy.Time.now() 255 | header.frame_id = 'map' 256 | # header.frame_id = str(self.pynt_stamp_idx) 257 | #create pcl from points 258 | scaled_polygon_pcl = pcl2.create_cloud_xyz32(header, cld_pynts) 259 | 260 | self.pynt_pub.publish(scaled_polygon_pcl) 261 | 262 | def calc_heading(self, pt1, pt2): 263 | pt_diff_x = pt2[0] - pt1[0] 264 | pt_diff_y = pt2[1] - pt1[1] 265 | yaw = np.arctan2(pt_diff_y, pt_diff_x) 266 | quat = quaternion_from_euler(0, 0, yaw) 267 | return quat 268 | 269 | 270 | def prepare_path_msg(self, path_seg): 271 | path_out = Path() 272 | path_out.header.stamp = rospy.Time.now() 273 | path_out.header.frame_id = "map" 274 | 275 | start_pos = path_seg[0] 276 | for nd_pos in path_seg: 277 | nd_pos_msg = PoseStamped() 278 | nd_pos_msg.pose.position.x = nd_pos[0] 279 | nd_pos_msg.pose.position.y = nd_pos[1] 280 | nd_pos_msg.pose.position.z = 1 281 | # calc heading 282 | if start_pos != nd_pos: 283 | quat = self.calc_heading(start_pos, nd_pos) 284 | nd_pos_msg.pose.orientation.w = 1 285 | nd_pos_msg.pose.orientation.x = 0 286 | nd_pos_msg.pose.orientation.y = 0 287 | nd_pos_msg.pose.orientation.z = 0 288 | else: 289 | nd_pos_msg.pose.orientation.w = 1 290 | nd_pos_msg.pose.orientation.x = 0 291 | nd_pos_msg.pose.orientation.y = 0 292 | nd_pos_msg.pose.orientation.z = 0 293 | 294 | path_out.poses.append(nd_pos_msg) 295 | 296 | return path_out 297 | 298 | 299 | 300 | def pub_temp_goal(self, nxt_pos = None): 301 | act_pos = activity_pose() 302 | act_pos.header = self.cmd_header 303 | 304 | if nxt_pos is None: 305 | # in case arrive target 306 | if self.LTLMP.next_wp is None: 307 | return 308 | nxt_pos = self.LTLMP.next_wp.pos 309 | 310 | goal_msg = PoseStamped() 311 | goal_msg.header.stamp = rospy.Time.now() 312 | goal_msg.header.frame_id = 'map' 313 | 314 | 315 | goal_msg.pose.orientation.w = 1 316 | goal_msg.pose.orientation.x = 0 317 | goal_msg.pose.orientation.y = 0 318 | goal_msg.pose.orientation.z = 0 319 | 320 | if type(nxt_pos) is str: 321 | act_pos.type = nxt_pos 322 | goal_msg.pose.position.x = self.rbt_pos[0] 323 | goal_msg.pose.position.y = self.rbt_pos[1] 324 | goal_msg.pose.position.z = self.rbt_pos[2] 325 | 326 | else: 327 | act_pos.type = "pass_by" 328 | goal_msg.pose.position.x = nxt_pos[0] 329 | goal_msg.pose.position.y = nxt_pos[1] 330 | goal_msg.pose.position.z = nxt_pos[2] 331 | 332 | 333 | self.cmd_header+=1 334 | 335 | act_pos.tgt_pose = goal_msg 336 | self.temp_goal_pub.publish(act_pos) 337 | -------------------------------------------------------------------------------- /LTLMP/src/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JINXER000/multiple-task-temporal-logic-planning/16bc21cac81b733efd0e657791ac492d2f0696fc/LTLMP/src/utils/__init__.py -------------------------------------------------------------------------------- /LTLMP/src/utils/misc_func.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import pybullet as p 4 | from pyb_plan_utils import connect, get_aabb_extent, load_model, disconnect, wait_if_gui, create_box, set_point, dump_body, \ 5 | TURTLEBOT_URDF, HideOutput, LockRenderer, joint_from_name, set_euler, get_euler, get_point, \ 6 | set_joint_position, get_joint_positions, pairwise_collision, stable_z, wait_for_duration, get_link_pose, \ 7 | link_from_name, get_pose, euler_from_quat, multiply, invert, draw_pose, unit_point, unit_quat, \ 8 | remove_debug, get_aabb, draw_aabb, get_subtree_aabb, ROOMBA_URDF, set_all_static, assign_link_colors, \ 9 | set_camera_pose, RGBA, draw_point, AABB, get_aabb_center, aabb_intersection 10 | 11 | # RGBA colors (alpha is transparency) 12 | RED = RGBA(1, 0, 0, 1) 13 | TAN = RGBA(0.824, 0.706, 0.549, 1) 14 | 15 | from itertools import product 16 | 17 | def get_automaton_dict(automaton, off_set=0): 18 | aut_nodes_dict = {} 19 | auto_nodes_ls = list(automaton.nodes) 20 | for i in range(len(auto_nodes_ls)): 21 | aut_nodes_dict[auto_nodes_ls[i]] = i + off_set 22 | 23 | return aut_nodes_dict 24 | 25 | 26 | def swap_dict(dictionary): 27 | return {value: key for key, value in dictionary.items()} 28 | 29 | # AABB defined in pyb_plan_utils 30 | 31 | class LineSegment: 32 | def __init__(self, start_point, end_point): 33 | self.start = start_point 34 | self.end = end_point 35 | 36 | def LinesegIntersectsAABB(aabb, line): 37 | tmin = 0.0 38 | tmax = 1.0 39 | 40 | for i in range(3): 41 | if abs(line.end[i] - line.start[i]) < 1e-9: # line segment is parallel to slab in this dimension 42 | if line.start[i] < aabb.lower[i] or line.start[i] > aabb.upper[i]: # line segment is outside of slab 43 | return False 44 | else: 45 | ood = 1.0 / (line.end[i] - line.start[i]) 46 | t1 = (aabb.lower[i] - line.start[i]) * ood 47 | t2 = (aabb.upper[i] - line.start[i]) * ood 48 | # t1 is the lower, t2 is the upper 49 | if t1 > t2: 50 | t1, t2 = t2, t1 51 | tmin = max(tmin, t1) 52 | tmax = min(tmax, t2) 53 | if tmin > tmax: 54 | return False 55 | 56 | return True 57 | 58 | # NOTE: if both points are inside obs, it will tell us NO COLLISION! 59 | def pybCheckCollision(p0, p1, dyn_obs_bdr= None): 60 | if p0 == p1: 61 | raise NameError("Cannot use it to check a point!") 62 | 63 | result = p.rayTest(p0, p1) 64 | if result[0][0] == -1: 65 | return False 66 | else: 67 | return True 68 | 69 | def collisionFreeAABB(p0, p1, workspace, dyn_obs_aabb= None): 70 | return not pybCheckCollision(p0, p1, dyn_obs_aabb) 71 | 72 | # not changing b_state if crossing 73 | for (obs, aabb) in iter(workspace.obstacles.items()): 74 | is_intersect = LinesegIntersectsAABB(aabb, LineSegment(p0, p1)) 75 | if is_intersect: 76 | return False 77 | 78 | # check dynamic obstacle 79 | if dyn_obs_aabb is None: 80 | return True 81 | 82 | for (d_obs, aabb) in iter(workspace.dyn_obs.items()): 83 | is_intersect = LinesegIntersectsAABB(aabb, LineSegment(p0, p1)) 84 | if is_intersect: 85 | return False 86 | return True 87 | 88 | def ptInObs(x, workspace): 89 | # obstacles 90 | for (obs, obs_AABB) in iter(workspace.obstacles.items()): 91 | if isWithinAABB(x, obs_AABB): 92 | return True 93 | return False 94 | 95 | 96 | def getLabel(x, workspace, is_hybrid = False): 97 | ret_label_set = set() 98 | 99 | # obstacles 100 | if ptInObs(x, workspace): 101 | ret_label_set.add('o') 102 | return ret_label_set 103 | 104 | # regions 105 | for (reg, reg_AABB) in iter(workspace.regions.items()): 106 | if isWithinAABB(x, reg_AABB): 107 | ret_lab = workspace.properties[reg] 108 | ret_label_set.add(ret_lab) 109 | 110 | # for hybrid 111 | if is_hybrid: 112 | media_lab = workspace.in_which_media(x) 113 | ret_label_set.add(media_lab) 114 | 115 | return ret_label_set 116 | 117 | def isWithinAABB(point, aabb): 118 | return all(min_coord <= point_coord <= max_coord for min_coord, point_coord, max_coord in zip(aabb.lower, point, aabb.upper)) 119 | 120 | def randomPointAABB(aabb): 121 | """ 122 | Samples a random point within an AABB. 123 | 124 | Args: 125 | aabb: A tuple of two lists of 3 floats each representing the min and max coordinates of the AABB. 126 | 127 | Returns: 128 | A list of 3 floats representing the coordinates of the sampled point. 129 | """ 130 | min_coords, max_coords = aabb 131 | rand_pt = [random.uniform(min_coord, max_coord) for min_coord, max_coord in zip(min_coords, max_coords)] 132 | return tuple(rand_pt) 133 | 134 | 135 | def fovSense(rbt_pos, fov_dist, workspace, reg_ls =[], obs_ls = [] ): 136 | fov_ll = [rbt_pos[i] - fov_dist * 0.5 for i in range(len(rbt_pos))] 137 | fov_ur = [rbt_pos[i] + fov_dist * 0.5 for i in range(len(rbt_pos))] 138 | fov_aabb = AABB(fov_ll, fov_ur) 139 | 140 | if len(reg_ls) and len(obs_ls): 141 | raise NameError('Cannot deal with them simultaneously!') 142 | 143 | if len(reg_ls): 144 | for reg_name in reg_ls: 145 | sensed = aabb_intersection(fov_aabb, workspace.regions[reg_name]) 146 | if sensed: 147 | return reg_name 148 | else: 149 | for obs_name in obs_ls: 150 | sensed = aabb_intersection(fov_aabb, workspace.dyn_obs[obs_name]) 151 | if sensed: 152 | return obs_name 153 | 154 | return None 155 | 156 | ############# pyb drawing 157 | 158 | def pyb_draw_sphere_marker(position, radius, color): 159 | vs_id = p.createVisualShape(p.GEOM_SPHERE, radius=radius, rgbaColor=color) 160 | marker_id = p.createMultiBody(basePosition=position, baseCollisionShapeIndex=-1, baseVisualShapeIndex=vs_id) 161 | return marker_id 162 | 163 | def pyb_remove_marker(marker_id): 164 | p.removeBody(marker_id) 165 | 166 | def pyb_draw_edges(edges, line_color=[0, 0, 1], line_width=1): 167 | for edge in edges: 168 | start = edge[0].pos 169 | end = edge[1].pos 170 | p.addUserDebugLine(start, end, line_color, line_width) 171 | 172 | def pyb_remove_edges(): 173 | p.removeAllUserDebugItems() 174 | 175 | 176 | 177 | def setup_pybullet(workspace, use_gui = False): 178 | # Creates a pybullet world and a visualizer for it 179 | connect(use_gui=use_gui) 180 | if use_gui: 181 | set_camera_pose(camera_point=[5, -5, 5], target_point=unit_point()) # Sets the camera's position 182 | identity_pose = (unit_point(), unit_quat()) 183 | origin_handles = draw_pose(identity_pose, 184 | length=1.0) # Draws the origin coordinate system (x:RED, y:GREEN, z:BLUE) 185 | 186 | 187 | # Bodies are described by an integer index 188 | floor = create_box(w=workspace.width, l=workspace.length, h=0.001, color=TAN) # Creates a tan box object for the floor 189 | set_point(floor, [0, 0, -0.001 / 2.]) # Sets the [x,y,z] translation of the floor 190 | 191 | for obs_aabb in workspace.obstacles.values(): 192 | center = get_aabb_center(obs_aabb) 193 | extent = get_aabb_extent(obs_aabb) 194 | obs = create_box(w = extent[0], l = extent[1], h= extent[2], color=RED) 195 | set_point(obs, center) 196 | print('Position:', get_point(obs)) 197 | # wait_for_duration(1.0) 198 | 199 | set_all_static() 200 | 201 | 202 | 203 | 204 | # generate pointcloud inside AABB 205 | def genPntCld(aabb, shrink_dis = 0): 206 | lower, upper = aabb 207 | ll_x, ll_y, ll_z = lower 208 | ur_x, ur_y, ur_z = upper 209 | X_list = range(int(ll_x+shrink_dis), int(ur_x+1-shrink_dis)) 210 | y_list = range(int(ll_y+shrink_dis), int(ur_y+1-shrink_dis)) 211 | z_list = range(int(ll_z+shrink_dis), int(ur_z+1-shrink_dis)) 212 | pnt_cld = list(product(X_list, y_list, z_list)) 213 | 214 | return pnt_cld 215 | 216 | 217 | def genWorkspacePntCld(workspace, bad_regs, dynobs_aabb): 218 | whole_cld = [] 219 | 220 | for block_reg in bad_regs: 221 | bad_aabb = workspace.regions[block_reg] 222 | cld = genPntCld(bad_aabb) 223 | whole_cld +=cld 224 | 225 | if dynobs_aabb is not None: 226 | cld = genPntCld(dynobs_aabb, shrink_dis = 0.1) 227 | whole_cld +=cld 228 | 229 | return whole_cld 230 | 231 | 232 | # for unit test 233 | if __name__ =='__main__': 234 | point = [5,5,5] 235 | from pyb_plan_utils import AABB 236 | 237 | aabb = AABB([0,0,0], [10,10,10]) 238 | if isWithinAABB(point, aabb): 239 | print('OK') 240 | else: 241 | print('fuck') 242 | -------------------------------------------------------------------------------- /LTLMP/src/utils/naive_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | class evaluator: 4 | def __init__(self): 5 | pass 6 | 7 | def euclideanDist(self, p0, p1): 8 | return np.linalg.norm(np.subtract(p0,p1)) 9 | 10 | def getCost(self, near_node, x_new): 11 | return self.euclideanDist(near_node.pos, x_new) 12 | 13 | def getHeuristic(self, x_new, goal_pos): 14 | return self.euclideanDist(goal_pos, x_new) -------------------------------------------------------------------------------- /LTLMP/src/utils/tree_visualizer.py: -------------------------------------------------------------------------------- 1 | import rospy 2 | from visualization_msgs.msg import Marker 3 | from geometry_msgs.msg import Point 4 | import numpy as np 5 | from tf.transformations import quaternion_from_euler 6 | 7 | 8 | class rviz_maker(object): 9 | def __init__(self, start_pos, end_pos, workspace, scale_factor, has_b_state): 10 | self.start_p = start_pos 11 | self.end_p = end_pos 12 | self.workspace = workspace 13 | self.scale = scale_factor 14 | self.has_b_state = has_b_state 15 | self.vertex_vis = Marker() 16 | self.changed_regs = set() 17 | self.marker_pub = rospy.Publisher("visualization_marker", Marker, queue_size=1) 18 | self.clear_all() 19 | 20 | def clear_all(self): 21 | edges_vis = Marker() 22 | edges_vis.header.frame_id = "map" 23 | edges_vis.header.stamp = rospy.rostime.Time.now() 24 | edges_vis.type = Marker.LINE_LIST 25 | edges_vis.action = Marker.DELETEALL 26 | edges_vis.ns = "edges" 27 | edges_vis.id = 3 28 | self.marker_pub.publish(edges_vis) 29 | 30 | vertex = Marker() 31 | vertex.header.frame_id = "map" 32 | vertex.header.stamp = rospy.rostime.Time.now() 33 | vertex.type = Marker.POINTS 34 | vertex.action = Marker.DELETEALL 35 | vertex.ns = "leaves" 36 | vertex.id = 4 37 | self.marker_pub.publish(vertex) 38 | 39 | rob_vis = Marker() 40 | rob_vis.header.frame_id = "map" 41 | rob_vis.header.stamp = rospy.rostime.Time.now() 42 | rob_vis.type = Marker.CUBE 43 | rob_vis.action = Marker.DELETEALL 44 | rob_vis.ns = "rbt" 45 | rob_vis.id = 1 46 | self.marker_pub.publish(rob_vis) 47 | 48 | fcs_vis = Marker() 49 | fcs_vis.header.frame_id = "map" 50 | fcs_vis.header.stamp = rospy.rostime.Time.now() 51 | fcs_vis.type = Marker.LINE_STRIP 52 | fcs_vis.action = Marker.DELETEALL 53 | fcs_vis.ns = "path" 54 | fcs_vis.id = 6 55 | self.marker_pub.publish(fcs_vis) 56 | 57 | vertex_vis = Marker() 58 | vertex_vis.header.frame_id = "map" 59 | vertex_vis.header.stamp = rospy.rostime.Time.now() 60 | vertex_vis.type = Marker.POINTS 61 | vertex_vis.action = Marker.ADD 62 | 63 | vertex_vis.ns = "rewired" 64 | vertex_vis.id = 4 65 | self.marker_pub.publish(vertex_vis) 66 | 67 | def display_edges(self, edge_list): 68 | edges_vis = Marker() 69 | edges_vis.header.frame_id = "map" 70 | edges_vis.header.stamp = rospy.rostime.Time.now() 71 | edges_vis.type = Marker.LINE_LIST 72 | edges_vis.action = Marker.ADD 73 | 74 | edges_vis.ns = "edges" 75 | edges_vis.id = 3 76 | 77 | edges_vis.scale.x = 0.02 78 | edges_vis.color.g = 0.5 79 | edges_vis.color.b = 0.5 80 | edges_vis.color.a = 0.7 81 | edges_vis.pose.orientation.w = 1 82 | 83 | invalid_edge_vis = Marker() 84 | invalid_edge_vis.header.frame_id = "map" 85 | invalid_edge_vis.header.stamp = rospy.rostime.Time.now() 86 | invalid_edge_vis.type = Marker.LINE_LIST 87 | invalid_edge_vis.action = Marker.ADD 88 | 89 | invalid_edge_vis.ns = "invalid_edges" 90 | invalid_edge_vis.id = 3 91 | 92 | invalid_edge_vis.scale.x = 0.02 93 | invalid_edge_vis.color.g = 0.2 94 | invalid_edge_vis.color.b = 0.2 95 | invalid_edge_vis.color.r = 0.5 96 | invalid_edge_vis.color.a = 0.7 97 | invalid_edge_vis.pose.orientation.w = 1 98 | 99 | BS_change_vis = Marker() 100 | BS_change_vis.header.frame_id = "map" 101 | BS_change_vis.header.stamp = rospy.rostime.Time.now() 102 | BS_change_vis.type = Marker.LINE_LIST 103 | BS_change_vis.action = Marker.ADD 104 | 105 | BS_change_vis.ns = "b_change_edges" 106 | BS_change_vis.id = 3 107 | 108 | BS_change_vis.scale.x = 0.02 109 | BS_change_vis.color.g = 0.5 110 | BS_change_vis.color.b = 0.0 111 | BS_change_vis.color.r = 1.0 112 | BS_change_vis.color.a = 0.7 113 | BS_change_vis.pose.orientation.w = 1 114 | 115 | for edge in edge_list: 116 | pt1 = Point() 117 | pt1.x = edge[0].pos[0] * self.scale 118 | pt1.y = edge[0].pos[1] * self.scale 119 | pt1.z = edge[0].pos[2] * self.scale 120 | pt2 = Point() 121 | pt2.x = edge[1].pos[0] * self.scale 122 | pt2.y = edge[1].pos[1] * self.scale 123 | pt2.z = edge[1].pos[2] * self.scale 124 | 125 | if self.has_b_state: 126 | pt1.z = int(edge[0].b_state) 127 | pt2.z = int(edge[1].b_state) 128 | 129 | if edge[0].cost2Root > 1e5 or edge[1].cost2Root > 1e5: 130 | 131 | invalid_edge_vis.points.append(pt1) 132 | invalid_edge_vis.points.append(pt2) 133 | continue 134 | elif edge[0].b_state is not None and edge[0].b_state != edge[1].b_state: 135 | BS_change_vis.points.append(pt1) 136 | BS_change_vis.points.append(pt2) 137 | continue 138 | 139 | if edge[0].cost2Root > 1e5 or edge[1].cost2Root > 1e5: 140 | 141 | invalid_edge_vis.points.append(pt1) 142 | invalid_edge_vis.points.append(pt2) 143 | else: 144 | edges_vis.points.append(pt1) 145 | edges_vis.points.append(pt2) 146 | 147 | self.marker_pub.publish(edges_vis) 148 | self.marker_pub.publish(invalid_edge_vis) 149 | self.marker_pub.publish(BS_change_vis) 150 | 151 | def draw_empty(self): 152 | vertex_vis = Marker() 153 | vertex_vis.header.frame_id = "map" 154 | vertex_vis.header.stamp = rospy.rostime.Time.now() 155 | vertex_vis.type = Marker.POINTS 156 | vertex_vis.action = Marker.ADD 157 | 158 | vertex_vis.ns = "rewired" 159 | vertex_vis.id = 4 160 | 161 | vertex_vis.scale.x = 0.06 162 | vertex_vis.color.r = 1.0 163 | vertex_vis.color.a = 1 164 | 165 | self.marker_pub.publish(vertex_vis) 166 | 167 | def draw_rewired_root(self, rewired_nodes): 168 | if len(rewired_nodes) == 0: 169 | return 170 | 171 | vertex_vis = Marker() 172 | vertex_vis.header.frame_id = "map" 173 | vertex_vis.header.stamp = rospy.rostime.Time.now() 174 | vertex_vis.type = Marker.POINTS 175 | vertex_vis.action = Marker.ADD 176 | 177 | vertex_vis.ns = "rewired" 178 | vertex_vis.id = 4 179 | 180 | vertex_vis.scale.x = 0.06 181 | vertex_vis.color.r = 1.0 182 | vertex_vis.color.a = 1 183 | 184 | for nd in rewired_nodes: 185 | p1 = nd.pos 186 | 187 | pt1 = Point() 188 | pt1.x = p1[0] * self.scale 189 | pt1.y = p1[1] * self.scale 190 | pt1.z = p1[2] * self.scale 191 | 192 | if self.has_b_state: 193 | pt1.z = int(self.nd.b_state) 194 | 195 | vertex_vis.points.append(pt1) 196 | 197 | self.marker_pub.publish(vertex_vis) 198 | 199 | def populateRviz(self): 200 | self.populateStart() 201 | self.populateEnd() 202 | self.populateWorkspace(self.workspace.regions, False) 203 | self.populateWorkspace(self.workspace.obstacles, True) 204 | self.draw_dyn_obs() 205 | 206 | def draw_ws_water(self): 207 | water_vis = Marker() 208 | water_vis.header.frame_id = "map" 209 | water_vis.header.stamp = rospy.rostime.Time.now() 210 | water_vis.type = Marker.CUBE 211 | water_vis.action = Marker.ADD 212 | 213 | water_vis.ns = "water" 214 | water_vis.id = 1 215 | 216 | water_vis.color.r = 0 217 | water_vis.color.g = 1 218 | water_vis.color.b = 1 219 | water_vis.color.a = 0.1 220 | 221 | water_vis.scale.x = 10 222 | water_vis.scale.y = 20 223 | water_vis.scale.z = 0.2 224 | water_vis.pose.position.x = 50 * self.scale 225 | water_vis.pose.position.y = 0 * self.scale 226 | 227 | water_vis.pose.position.z = 0 228 | 229 | water_vis.pose.orientation.w = 1 230 | self.marker_pub.publish(water_vis) 231 | 232 | def populateExt(self, coords): 233 | bdr = Marker() 234 | bdr.header.frame_id = "map" 235 | bdr.header.stamp = rospy.rostime.Time.now() 236 | bdr.type = Marker.LINE_LIST 237 | bdr.action = Marker.ADD 238 | bdr.ns = "ws_ext" 239 | bdr.id = 0 240 | 241 | bdr.scale.x = 0.2 242 | bdr.color.r = 0.6 243 | bdr.color.g = 0.6 244 | bdr.color.b = 0.9 245 | bdr.color.a = 0.8 246 | bdr.pose.orientation.w = 1 247 | 248 | for idx in range(len(coords) - 1): 249 | pt0 = Point() 250 | pt1 = Point() 251 | pt0.x = coords[idx][0] * self.scale 252 | pt0.y = coords[idx][1] * self.scale 253 | pt1.x = coords[idx + 1][0] * self.scale 254 | pt1.y = coords[idx + 1][1] * self.scale 255 | bdr.points.append(pt1) 256 | bdr.points.append(pt0) 257 | self.marker_pub.publish(bdr) 258 | 259 | def populateWorkspace(self, regions, is_obs=True): 260 | idx_offset = 5 261 | if is_obs: 262 | idx_offset += 10 263 | for key in regions.keys(): 264 | aabb = regions[key] 265 | lower, upper = aabb 266 | coords = np.array([(lower[0], lower[1]), (upper[0], lower[1]), (upper[0], upper[1]), (lower[0], upper[1]), (lower[0], lower[1])]) 267 | 268 | # x, y = poly.exterior.xy 269 | # coords = np.column_stack((x, y)) # coords = [(0, 0), (1, 1), (1, 0)] 270 | 271 | bdr = Marker() 272 | bdr.header.frame_id = "map" 273 | bdr.header.stamp = rospy.rostime.Time.now() 274 | bdr.type = Marker.LINE_LIST 275 | bdr.action = Marker.ADD 276 | bdr.ns = "regs" 277 | bdr.id = idx_offset 278 | idx_offset += 1 279 | 280 | bdr.scale.x = 0.2 281 | if is_obs: 282 | bdr.color.r = 0.8 283 | bdr.color.g = 0.3 284 | bdr.color.b = 0.3 285 | elif key in self.changed_regs: 286 | bdr.color.r = 0.6 287 | bdr.color.g = 0.6 288 | bdr.color.b = 0.0 289 | else: 290 | bdr.color.r = 0.3 291 | bdr.color.g = 1 292 | bdr.color.b = 0.3 293 | bdr.color.a = 0.8 294 | bdr.pose.orientation.w = 1 295 | 296 | for idx in range(len(coords) - 1): 297 | pt0 = Point() 298 | pt1 = Point() 299 | pt0.x = coords[idx][0] * self.scale 300 | pt0.y = coords[idx][1] * self.scale 301 | pt1.x = coords[idx + 1][0] * self.scale 302 | pt1.y = coords[idx + 1][1] * self.scale 303 | bdr.points.append(pt1) 304 | bdr.points.append(pt0) 305 | 306 | self.marker_pub.publish(bdr) 307 | 308 | txt = Marker() 309 | # compute center 310 | x_center = sum([pt[0] for pt in coords]) / len(coords) 311 | y_center = sum([pt[1] for pt in coords]) / len(coords) 312 | 313 | # place text 314 | txt.header.frame_id = "map" 315 | txt.header.stamp = rospy.rostime.Time.now() 316 | txt.type = Marker.TEXT_VIEW_FACING 317 | txt.ns = "text" 318 | txt.id = idx_offset 319 | idx_offset += 1 320 | txt.action = Marker.ADD 321 | txt.pose.position.x = x_center * self.scale 322 | txt.pose.position.y = y_center * self.scale 323 | txt.pose.orientation.w = 1 324 | 325 | txt.scale.z = 1 326 | if is_obs: 327 | txt.color.r = 0 328 | txt.color.g = 0 329 | txt.color.b = 0 330 | txt.text = 'obs' 331 | else: 332 | txt.color.r = 0 333 | txt.color.g = 0 334 | txt.color.b = 0 335 | txt.text = key 336 | # txt.text = properties[key] 337 | txt.color.a = 1 338 | 339 | # self.txt_vis_list.append(txt) 340 | self.marker_pub.publish(txt) 341 | 342 | def display_text(self, text): 343 | txt = Marker() 344 | # compute center 345 | x_center = 50 346 | y_center = 50 347 | 348 | # place text 349 | txt.header.frame_id = "map" 350 | txt.header.stamp = rospy.rostime.Time.now() 351 | txt.type = Marker.TEXT_VIEW_FACING 352 | txt.ns = "user_text" 353 | txt.id = 0 354 | txt.action = Marker.ADD 355 | txt.pose.position.x = x_center * self.scale 356 | txt.pose.position.y = y_center * self.scale 357 | txt.pose.orientation.w = 1 358 | 359 | txt.scale.z = 0.7 360 | txt.color.r = 0.7 361 | txt.color.g = 0.8 362 | txt.color.b = 0.95 363 | txt.text = text 364 | txt.color.a = 1.0 365 | 366 | self.marker_pub.publish(txt) 367 | 368 | def populateStart(self): 369 | if self.start_p is None: 370 | return 371 | 372 | pt1 = Point() 373 | pt1.x = self.start_p[0] * self.scale 374 | pt1.y = self.start_p[1] * self.scale 375 | pt1.z = self.start_p[2] * self.scale 376 | 377 | vertex = Marker() 378 | vertex.header.frame_id = "map" 379 | vertex.header.stamp = rospy.rostime.Time.now() 380 | vertex.type = Marker.POINTS 381 | vertex.action = Marker.ADD 382 | 383 | vertex.ns = "start" 384 | vertex.id = 0 385 | 386 | vertex.scale.x = vertex.scale.y = vertex.scale.z = 0.16 387 | 388 | vertex.color.g = 1.0 389 | vertex.color.a = 1 390 | 391 | vertex.points.append(pt1) 392 | 393 | self.marker_pub.publish(vertex) 394 | 395 | def populateEnd(self): 396 | if self.end_p is None: 397 | return 398 | 399 | vertex = Marker() 400 | vertex.header.frame_id = "map" 401 | vertex.header.stamp = rospy.rostime.Time.now() 402 | vertex.type = Marker.CUBE 403 | vertex.action = Marker.ADD 404 | 405 | vertex.ns = "end" 406 | vertex.id = 1 407 | 408 | vertex.scale.x = vertex.scale.y = vertex.scale.z = 0.1 409 | vertex.color.g = 1.0 410 | vertex.color.r = 0.7 411 | vertex.color.a = 1 412 | 413 | vertex.pose.position.x = self.end_p[0] * self.scale 414 | vertex.pose.position.y = self.end_p[1] * self.scale 415 | 416 | self.marker_pub.publish(vertex) 417 | 418 | def draw_rtroot(self, root): 419 | if root is None: 420 | return 421 | 422 | vertex = Marker() 423 | vertex.header.frame_id = "map" 424 | vertex.header.stamp = rospy.rostime.Time.now() 425 | vertex.type = Marker.CUBE 426 | vertex.action = Marker.ADD 427 | 428 | vertex.ns = "rt_root" 429 | vertex.id = 1 430 | 431 | vertex.scale.x = vertex.scale.y = vertex.scale.z = 0.1 432 | vertex.color.g = 1.0 433 | vertex.color.r = 0.7 434 | vertex.color.a = 1 435 | 436 | vertex.pose.position.x = root.pos[0] * self.scale 437 | vertex.pose.position.y = root.pos[1] * self.scale 438 | vertex.pose.position.z = root.pos[2] * self.scale 439 | if self.has_b_state: 440 | vertex.pose.position.z = int(root.b_state) 441 | 442 | self.marker_pub.publish(vertex) 443 | 444 | def drawSample(self, x_rand, near_range): 445 | vertex = Marker() 446 | vertex.header.frame_id = "map" 447 | vertex.header.stamp = rospy.rostime.Time.now() 448 | vertex.type = Marker.CYLINDER 449 | vertex.action = Marker.ADD 450 | 451 | vertex.ns = "sample" 452 | vertex.id = 2 453 | 454 | vertex.scale.x = vertex.scale.y = vertex.scale.z = 0.1 455 | vertex.color.b = 0.2 456 | vertex.color.g = 1.0 457 | vertex.color.r = 1.0 458 | vertex.color.a = 1 459 | 460 | vertex.pose.position.x = x_rand[0] * self.scale 461 | vertex.pose.position.y = x_rand[1] * self.scale 462 | 463 | self.marker_pub.publish(vertex) 464 | 465 | circle = Marker() 466 | circle.header.frame_id = "map" 467 | circle.header.stamp = rospy.rostime.Time.now() 468 | circle.type = Marker.SPHERE 469 | circle.action = Marker.ADD 470 | 471 | circle.ns = "near_range" 472 | circle.id = 1 473 | 474 | circle.scale.x = circle.scale.y = near_range * self.scale 475 | circle.scale.z = 0.05 476 | circle.color.b = circle.color.g = circle.color.r = 0.2 477 | circle.color.a = 0.2 478 | 479 | circle.pose.position.x = vertex.pose.position.x 480 | circle.pose.position.y = vertex.pose.position.y 481 | 482 | self.marker_pub.publish(circle) 483 | 484 | def drawFinalPath(self, path): 485 | # if len(path)<2: 486 | # return 487 | 488 | fcs_vis = Marker() 489 | fcs_vis.header.frame_id = "map" 490 | fcs_vis.header.stamp = rospy.rostime.Time.now() 491 | fcs_vis.type = Marker.LINE_STRIP 492 | fcs_vis.action = Marker.ADD 493 | fcs_vis.ns = "path" 494 | fcs_vis.id = 6 495 | 496 | fcs_vis.scale.x = 0.8 497 | fcs_vis.color.g = 0.9 498 | fcs_vis.color.b = 0.5 499 | fcs_vis.color.a = 0.2 500 | fcs_vis.pose.orientation.w = 1 501 | 502 | for p1 in path: 503 | pt1 = Point() 504 | pt1.x = p1.pos[0] * self.scale 505 | pt1.y = p1.pos[1] * self.scale 506 | pt1.z = p1.pos[2] * self.scale 507 | 508 | if self.has_b_state: 509 | pt1.z = int(p1.b_state) 510 | 511 | fcs_vis.points.append(pt1) 512 | 513 | self.marker_pub.publish(fcs_vis) 514 | 515 | def drawPrefixPath(self, path, motion_bias=None): 516 | if len(path) < 2: 517 | return 518 | 519 | fcs_vis = Marker() 520 | fcs_vis.header.frame_id = "map" 521 | fcs_vis.header.stamp = rospy.rostime.Time.now() 522 | fcs_vis.type = Marker.LINE_STRIP 523 | fcs_vis.action = Marker.ADD 524 | fcs_vis.ns = "path" 525 | fcs_vis.id = 6 526 | 527 | fcs_vis.scale.x = 0.8 528 | fcs_vis.color.g = 0.9 529 | fcs_vis.color.b = 0.5 530 | fcs_vis.color.a = 0.2 531 | fcs_vis.pose.orientation.w = 1 532 | 533 | for p1 in path: 534 | pt1 = Point() 535 | pt1.x = p1[0][0][0] + motion_bias[0] 536 | pt1.y = p1[0][0][1] + motion_bias[1] 537 | pt1.z = p1[0][0][2] + motion_bias[1] 538 | 539 | if self.has_b_state: 540 | pt1.z = int(p1[1]) 541 | 542 | fcs_vis.points.append(pt1) 543 | 544 | self.marker_pub.publish(fcs_vis) 545 | 546 | def draw_rbt(self, rbt_pos, cur_heading, fov_range, b_state=None): 547 | rob_vis = Marker() 548 | rob_vis.header.frame_id = "map" 549 | rob_vis.header.stamp = rospy.rostime.Time.now() 550 | rob_vis.type = Marker.CUBE 551 | rob_vis.action = Marker.ADD 552 | 553 | rob_vis.ns = "rbt" 554 | rob_vis.id = 1 555 | 556 | rob_vis.scale.x = rob_vis.scale.y = rob_vis.scale.z = 0.1 557 | 558 | rob_vis.color.r = 1 559 | rob_vis.color.g = 0.2 560 | rob_vis.color.b = 1 561 | rob_vis.color.a = 0.8 562 | 563 | rob_vis.scale.x = 0.5 564 | rob_vis.scale.y = 0.3 565 | rob_vis.scale.z = 0.2 566 | rob_vis.pose.position.x = rbt_pos[0] * self.scale 567 | rob_vis.pose.position.y = rbt_pos[1] * self.scale 568 | rob_vis.pose.position.z = rbt_pos[2] * self.scale 569 | if b_state: 570 | rob_vis.pose.position.z = int(b_state) 571 | yaw = np.arctan2(cur_heading[1], cur_heading[0]) 572 | quat = quaternion_from_euler(0, 0, yaw) 573 | rob_vis.pose.orientation.x = quat[0] 574 | rob_vis.pose.orientation.y = quat[1] 575 | rob_vis.pose.orientation.z = quat[2] 576 | rob_vis.pose.orientation.w = quat[3] 577 | self.marker_pub.publish(rob_vis) 578 | 579 | circle = Marker() 580 | circle.header.frame_id = "map" 581 | circle.header.stamp = rospy.rostime.Time.now() 582 | circle.type = Marker.CUBE 583 | circle.action = Marker.ADD 584 | 585 | circle.ns = "fov_range" 586 | circle.id = 1 587 | 588 | circle.scale.x = circle.scale.y = fov_range * self.scale 589 | circle.scale.z = 0.1 590 | circle.color.b = circle.color.g = 0.2 591 | circle.color.r = 0.5 592 | circle.color.a = 0.1 593 | 594 | circle.pose.position.x = rob_vis.pose.position.x 595 | circle.pose.position.y = rob_vis.pose.position.y 596 | self.marker_pub.publish(circle) 597 | 598 | def draw_leaves(self, node_ls): 599 | 600 | vertex = Marker() 601 | vertex.header.frame_id = "map" 602 | vertex.header.stamp = rospy.rostime.Time.now() 603 | vertex.type = Marker.POINTS 604 | vertex.action = Marker.ADD 605 | 606 | vertex.ns = "leaves" 607 | vertex.id = 4 608 | 609 | vertex.scale.x = vertex.scale.y = vertex.scale.z = 0.05 610 | 611 | vertex.color.b = 1.0 612 | vertex.color.a = 1 613 | 614 | for nd in node_ls: 615 | pt1 = Point() 616 | pt1.x = nd.pos[0] * self.scale 617 | pt1.y = nd.pos[1] * self.scale 618 | pt1.z = nd.pos[2] * self.scale 619 | 620 | if self.has_b_state: 621 | pt1.z = int(nd.b_state) 622 | 623 | vertex.points.append(pt1) 624 | 625 | self.marker_pub.publish(vertex) 626 | 627 | def draw_dyn_obs(self): 628 | vertex = Marker() 629 | vertex.header.frame_id = "map" 630 | vertex.header.stamp = rospy.rostime.Time.now() 631 | vertex.type = Marker.CUBE 632 | vertex.action = Marker.ADD 633 | 634 | vertex.ns = "dyn_obs" 635 | vertex.id = 2 636 | 637 | vertex.scale.x = (self.workspace.do_ur[0] - self.workspace.do_ll[0]) * self.scale 638 | vertex.scale.y = (self.workspace.do_ur[1] - self.workspace.do_ll[1]) * self.scale 639 | vertex.scale.z = 0.8 640 | vertex.color.b = 0.5 641 | vertex.color.g = 0.5 642 | vertex.color.r = 0.8 643 | vertex.color.a = 0.8 644 | 645 | vertex.pose.position.x = self.workspace.dynamic_obs_center[0] * self.scale 646 | vertex.pose.position.y = self.workspace.dynamic_obs_center[1] * self.scale 647 | 648 | self.marker_pub.publish(vertex) 649 | 650 | 651 | if __name__ == '__main__': 652 | from workspace import Workspace3D 653 | 654 | rospy.init_node('visualizer', anonymous=False) 655 | wksp = Workspace3D() 656 | rviz_mgr = rviz_maker(None, None, wksp, scale_factor=1, has_b_state= False) 657 | 658 | # ws_ext_coords = getAABB(wksp.workspace_ll, ws_ur) 659 | # ws_ext_coords.append(wksp.workspace_ll) 660 | 661 | while not rospy.is_shutdown(): 662 | rviz_mgr.populateWorkspace(rviz_mgr.workspace.regions, False) 663 | rviz_mgr.populateWorkspace(rviz_mgr.workspace.obstacles, True) 664 | rviz_mgr.draw_dyn_obs() 665 | # rviz_mgr.populateExt(ws_ext_coords) 666 | 667 | rospy.sleep(0.1) 668 | 669 | # rospy.spin() -------------------------------------------------------------------------------- /LTLMP/src/utils/workspace.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import random 3 | 4 | import sys 5 | sys.path.append("/home/joseph/yzchen_ws/task_planning/AI-plan/pybullet-planning/") 6 | sys.path.append("/home/joseph/yzchen_ws/task_planning/ltl_mq_opensource/src/pybullet-planning") 7 | sys.path.append("/home/joseph/yzchen_ws/task_planning/ltl_mq_opensource/src/LTLMP/src/utils/") 8 | 9 | from pyb_plan_utils import AABB 10 | from misc_func import setup_pybullet 11 | 12 | 13 | def calc_ur_given_ll(ll_coords, reg_side): 14 | ur_coords = [] 15 | for ll_crd in ll_coords: 16 | ur_crd = [ll_crd[i] + reg_side for i in range(len(ll_crd))] 17 | ur_coords.append(ur_crd) 18 | 19 | return ur_coords 20 | 21 | def calc_reg_center(ll_coord, reg_len): 22 | return (ll_coord[0]+ 0.5*reg_len, ll_coord[1]+ 0.5*reg_len) 23 | 24 | 25 | 26 | 27 | class Workspace3D(object): 28 | def __init__(self): 29 | self.length = 7.6 30 | self.width = 6.2 31 | self.height = 2.2 32 | self.workspace_range = (self.length, self.width, self.height) 33 | self.workspace_ll = (-3.4, -3.0, 0.) 34 | self.workspace_ur = (self.workspace_ll[0]+ self.length, self.workspace_ll[1]+ self.width, self.workspace_ll[2] + self.height) 35 | self.ws_AABB = AABB(self.workspace_ll, self.workspace_ur) 36 | reg_side = 1.2 37 | 38 | # regions 39 | reg_ll_coords = [(2.0, 1.0, 0.0), (-1.0, -3.0, 1.), (-3.0, 0.0, 0.0)] 40 | reg_ur_coords = calc_ur_given_ll(reg_ll_coords, reg_side) 41 | self.regions = {'l1': AABB(reg_ll_coords[0], reg_ur_coords[0]), 42 | 'l2': AABB(reg_ll_coords[1], reg_ur_coords[1]), 43 | 'l3': AABB(reg_ll_coords[2], reg_ur_coords[2])} 44 | 45 | self.reg_set = {k for k in self.regions} 46 | self.properties = {'l1': 'grassland', 'l2': 'pond', 'l3': 'grassland'} 47 | 48 | self.reg_centers = { 49 | 'l1': tuple((ll + ur) / 2 for ll, ur in zip(reg_ll_coords[0], reg_ur_coords[0])), 50 | 'l2':tuple((ll + ur) / 2 for ll, ur in zip(reg_ll_coords[1], reg_ur_coords[1])), 51 | 'l3': tuple((ll + ur) / 2 for ll, ur in zip(reg_ll_coords[2], reg_ur_coords[2])), 52 | } 53 | 54 | # obstacles 55 | obs_ll_coords = [(-3.4, -3.0, 0.), (2.0, -3.0, 0.), (-1.0, 1.0, 0)] 56 | obs_ur_coords = [(-2.0, -2.0, 1.0), (4.0, -1.5, 1.0), (0.0, 3.0, 0.2)] 57 | self.obstacles = {'o1': AABB(obs_ll_coords[0], obs_ur_coords[0]), 58 | 'o2': AABB(obs_ll_coords[1], obs_ur_coords[1]), 59 | 'o3': AABB(obs_ll_coords[2], obs_ur_coords[2])} 60 | 61 | # dynamic obstacle 62 | self.do_ll, self.do_ur = (-50, -30,10), (-15, -20, 10) 63 | self.dyn_obs = {'d1': AABB(self.do_ll, self.do_ur) } 64 | self.dynamic_obs_center = ((self.do_ur[0] +self.do_ll[0])* 0.5, (self.do_ur[1] +self.do_ll[1])* 0.5, (self.do_ur[2] +self.do_ll[2])* 0.5) 65 | 66 | 67 | 68 | 69 | if __name__ == '__main__': 70 | wksp = Workspace3D() 71 | setup_pybullet(wksp, use_gui=False) 72 | # wksp.test_scene() 73 | -------------------------------------------------------------------------------- /figure/coverpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JINXER000/multiple-task-temporal-logic-planning/16bc21cac81b733efd0e657791ac492d2f0696fc/figure/coverpage.png -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | The software for IROS2023 paper: An interactive system for multiple-task linear temporal logic path planning. 2 | 3 | 4 | 5 | 6 | The supplementary video can be viewed here: 7 | 8 |

9 | MT-LTL  introduction video 12 |

13 | 14 | 15 | # functionality 16 | - [x] Sampling-based LTL path planning 17 | - [x] Task switching 18 | - [ ] Human-machine interface 19 | 20 | # install 21 | ## 3rd party dependancies 22 | ``` 23 | pip install numpy networkx itertools matplotlib ply pybullet 24 | ``` 25 | ## ltl2dfa 26 | ``` 27 | sudo apt-get install flex -y 28 | sudo apt-get install bison -yx 29 | sudo apt-get install byacc -y 30 | git clone https://github.com/whitemech/LTLf2DFA.git 31 | cd ltlf2dfa 32 | pip install . 33 | ``` 34 | ## ltl2ba 35 | follow $(PROJECT_PATH)/src/formula_parser/install_ltl2ba/README.txt. 36 | 37 | 38 | # Folder configuration 39 | Replace folder name in workspace.py and gazebo_HMI_plugin.py. A quick way is to search '/home/joseph/yzchen_ws'. 40 | 41 | 42 | # Experiment with the whole system 43 | 44 | ## [optional when doing simulation] launch a gazebo environment 45 | ## run modules like localization, mapping, and motion planning 46 | ``` 47 | ./start nndp 48 | ``` 49 | ## launch HMI 50 | ``` 51 | cd /home/joseph/yzchen_ws/task_planning/ltl_interface/ 52 | source devel/setup.bash 53 | rqt 54 | ``` 55 | Then open the plugin in rqt: 'LTL task planning' 56 | ## launch the teleoperation service 57 | ``` 58 | cd /home/joseph/yzchen_ws/task_planning/ltl_interface/ 59 | source devel/setup.bash 60 | python /home/joseph/yzchen_ws/task_planning/ltl_interface/src/remote_server/src/remote_launch_service.py 61 | ``` 62 | ## launch the LTL planner 63 | ``` 64 | python gazebo_HMI_plugin.py 65 | ``` 66 | ## Get ready for the task (e.g., taking off, do system selection in HMI) 67 | ## Input the LTL specification by voice or text 68 | 69 | # credit 70 | The body of code in pyb_plan_utils.py comes from https://github.com/caelan/pybullet-planning.git. --------------------------------------------------------------------------------