From effa653e30d8c1e5f060d6d7bfb797da51b083be Mon Sep 17 00:00:00 2001 From: Caiyishuai <39987654+Caiyishuai@users.noreply.github.com> Date: Mon, 6 Nov 2023 17:31:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=BA=86=E5=AE=8C=E5=85=A8?= =?UTF-8?q?=E6=9C=80=E4=BC=98=E8=A1=8C=E4=B8=BA=E6=A0=91=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E7=AE=97=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 初始化传入 actions,运行时传入 goal,返回完全最优行为树的 ptml 表示 --- opt_bt_expansion/BehaviorTree.py | 95 ++++++++ opt_bt_expansion/MakeCoffee.ptml | 59 +++++ .../OptimalBTExpansionAlgorithm.py | 210 ++++++++++++++++++ .../README.assets/image-20231103191141047.png | Bin 0 -> 55821 bytes opt_bt_expansion/README.md | 121 ++++++++++ opt_bt_expansion/examples.py | 174 +++++++++++++++ opt_bt_expansion/opt_bt_exp_main.py | 121 ++++++++++ opt_bt_expansion/tools.py | 167 ++++++++++++++ 8 files changed, 947 insertions(+) create mode 100644 opt_bt_expansion/BehaviorTree.py create mode 100644 opt_bt_expansion/MakeCoffee.ptml create mode 100644 opt_bt_expansion/OptimalBTExpansionAlgorithm.py create mode 100644 opt_bt_expansion/README.assets/image-20231103191141047.png create mode 100644 opt_bt_expansion/README.md create mode 100644 opt_bt_expansion/examples.py create mode 100644 opt_bt_expansion/opt_bt_exp_main.py create mode 100644 opt_bt_expansion/tools.py diff --git a/opt_bt_expansion/BehaviorTree.py b/opt_bt_expansion/BehaviorTree.py new file mode 100644 index 0000000..4513a20 --- /dev/null +++ b/opt_bt_expansion/BehaviorTree.py @@ -0,0 +1,95 @@ + +#叶结点 +class Leaf: + def __init__(self,type,content,mincost): + self.type=type + self.content=content #conditionset or action + self.parent=None + self.parent_index=0 + self.mincost=mincost + + # tick 叶节点,返回返回值以及对应的条件或行动对象self.content + def tick(self,state): + if self.type=='cond': + if self.content <= state: + return 'success',self.content + else: + return 'failure',self.content + if self.type=='act': + if self.content.pre<=state: + return 'running',self.content #action + else: + return 'failure',self.content + + def __str__(self): + print( self.content) + return '' + + def print_nodes(self): + print(self.content) + + def count_size(self): + return 1 + + +#可能包含控制结点的行为树 +class ControlBT: + def __init__(self,type): + self.type=type + self.children=[] + self.parent=None + self.parent_index=0 + + + def add_child(self,subtree_list): + for subtree in subtree_list: + self.children.append(subtree) + subtree.parent=self + subtree.parent_index=len(self.children)-1 + + # tick行为树,根据不同控制结点逻辑tick子结点 + def tick(self,state): + if len(self.children) < 1: + print("error,no child") + if self.type =='?':#选择结点,即或结点 + for child in self.children: + val,obj=child.tick(state) + if val=='success': + return val,obj + if val=='running': + return val,obj + return 'failure','?fails' + if self.type =='>':#顺序结点,即与结点 + for child in self.children: + val,obj=child.tick(state) + if val=='failure': + return val,obj + if val=='running': + return val,obj + return 'success', '>success' + if self.type =='act':#行动结点 + return self.children[0].tick(state) + if self.type =='cond':#条件结点 + return self.children[0].tick(state) + + def getFirstChild(self): + return self.children[0] + + def __str__(self): + print(self.type+'\n') + for child in self.children: + print (child) + return '' + + def print_nodes(self): + print(self.type) + for child in self.children: + child.print_nodes() + + # 递归统计树中结点数 + def count_size(self): + result=1 + for child in self.children: + result+= child.count_size() + return result + diff --git a/opt_bt_expansion/MakeCoffee.ptml b/opt_bt_expansion/MakeCoffee.ptml new file mode 100644 index 0000000..40fc6a4 --- /dev/null +++ b/opt_bt_expansion/MakeCoffee.ptml @@ -0,0 +1,59 @@ +selector{ +cond At(Table,Coffee) +selector{ +cond At(Robot,Table), Holding(Coffee) +act PutDown(Table,Coffee) +} +selector{ +cond At(Robot,Coffee), NotHolding, At(Robot,Table) +act PickUp(Coffee) +} +selector{ +cond Available(Table), Holding(Coffee) +act MoveTo(Table) +} +selector{ +cond At(Robot,Coffee), At(Robot,Table), Holding(VacuumCup) +act PutDown(Table,VacuumCup) +} +selector{ +cond At(Robot,CoffeeMachine), NotHolding, At(Robot,Table) +act OpCoffeeMachine +} +selector{ +cond At(Robot,Coffee), Available(Table), NotHolding +act PickUp(Coffee) +} +selector{ +cond At(Robot,CoffeeMachine), At(Robot,Table), Holding(VacuumCup) +act PutDown(Table,VacuumCup) +} +selector{ +cond Available(Table), Available(Coffee), NotHolding +act MoveTo(Coffee) +} +selector{ +cond Available(Table), At(Robot,CoffeeMachine), NotHolding +act OpCoffeeMachine +} +selector{ +cond Available(Table), Available(Coffee), At(Robot,Table), Holding(VacuumCup) +act PutDown(Table,VacuumCup) +} +selector{ +cond Available(Table), NotHolding, Available(CoffeeMachine) +act MoveTo(CoffeeMachine) +} +selector{ +cond Available(Table), Available(Coffee), Holding(VacuumCup) +act MoveTo(Table) +} +selector{ +cond Available(Table), Available(CoffeeMachine), At(Robot,Table), Holding(VacuumCup) +act PutDown(Table,VacuumCup) +} +selector{ +cond Available(Table), Available(CoffeeMachine), Holding(VacuumCup) +act MoveTo(Table) +} +} diff --git a/opt_bt_expansion/OptimalBTExpansionAlgorithm.py b/opt_bt_expansion/OptimalBTExpansionAlgorithm.py new file mode 100644 index 0000000..00528cf --- /dev/null +++ b/opt_bt_expansion/OptimalBTExpansionAlgorithm.py @@ -0,0 +1,210 @@ +import copy +import random +from opt_bt_expansion.BehaviorTree import Leaf,ControlBT + +class CondActPair: + def __init__(self, cond_leaf,act_leaf): + self.cond_leaf = cond_leaf + self.act_leaf = act_leaf + +#定义行动类,行动包括前提、增加和删除影响 +class Action: + def __init__(self,name='anonymous action',pre=set(),add=set(),del_set=set(),cost=1): + self.pre=copy.deepcopy(pre) + self.add=copy.deepcopy(add) + self.del_set=copy.deepcopy(del_set) + self.name=name + self.cost=cost + + def __str__(self): + return self.name + +#生成随机状态 +def generate_random_state(num): + result = set() + for i in range(0,num): + if random.random()>0.5: + result.add(i) + return result +#从状态和行动生成后继状态 +def state_transition(state,action): + if not action.pre <= state: + print ('error: action not applicable') + return state + new_state=(state | action.add) - action.del_set + return new_state + + +#本文所提出的完备规划算法 +class OptBTExpAlgorithm: + def __init__(self,verbose=False): + self.bt = None + self.nodes=[] + self.traversed=[] + self.mounted=[] + self.conditions=[] + self.conditions_index=[] + self.verbose=verbose + + def clear(self): + self.bt = None + self.nodes = [] + self.traversed = [] + self.conditions = [] + self.conditions_index = [] + + #运行规划算法,从初始状态、目标状态和可用行动,计算行为树self.bt + def run_algorithm(self,goal,actions): + if self.verbose: + print("\n算法开始!") + + + self.bt = ControlBT(type='cond') + # 初始行为树只包含目标条件 + gc_node = Leaf(type='cond', content=goal,mincost=0) # 为了统一,都成对出现 + ga_node = Leaf(type='act', content=None, mincost=0) + subtree = ControlBT(type='?') + subtree.add_child([copy.deepcopy(gc_node)]) # 子树首先保留所扩展结 + self.bt.add_child([subtree]) + + # self.conditions.append(goal) + cond_anc_pair = CondActPair(cond_leaf=gc_node,act_leaf=ga_node) + self.nodes.append(copy.deepcopy(cond_anc_pair)) # the set of explored but unexpanded conditions + self.traversed = [goal] # the set of expanded conditions + + + while len(self.nodes)!=0: + + # Find the condition for the shortest cost path + pair_node = None + min_cost = float ('inf') + index= -1 + for i,cond_anc_pair in enumerate(self.nodes): + if cond_anc_pair.cond_leaf.mincost < min_cost: + min_cost = cond_anc_pair.cond_leaf.mincost + pair_node = copy.deepcopy(cond_anc_pair) + index = i + break + + if self.verbose: + print("选择扩展条件结点:",pair_node.cond_leaf.content) + # Update self.nodes and self.traversed + self.nodes.pop(index) # the set of explored but unexpanded conditions. self.nodes.remove(pair_node) + c = pair_node.cond_leaf.content # 子树所扩展结点对应的条件(一个文字的set) + + # Mount the action node and extend BT. T = Eapand(T,c,A(c)) + if c!=goal: + sequence_structure = ControlBT(type='>') + sequence_structure.add_child( + [copy.deepcopy(pair_node.cond_leaf), copy.deepcopy(pair_node.act_leaf)]) + subtree.add_child([copy.deepcopy(sequence_structure)]) # subtree 是回不断变化的,它的父亲是self.bt + if self.verbose: + print("完成扩展 a_node= %s,对应的新条件 c_attr= %s,mincost=%d" \ + % (cond_anc_pair.act_leaf.content.name, cond_anc_pair.cond_leaf.content, + cond_anc_pair.cond_leaf.mincost)) + + if self.verbose: + print("遍历所有动作, 寻找符合条件的动作") + # 遍历所有动作, 寻找符合条件的动作 + current_mincost = pair_node.cond_leaf.mincost # 当前的最短路径是多少 + + for i in range(0, len(actions)): + if not c & ((actions[i].pre | actions[i].add) - actions[i].del_set) <= set(): + if (c - actions[i].del_set) == c: + if self.verbose: + print("———— 满足条件可以扩展") + c_attr = (actions[i].pre | c) - actions[i].add + + # 剪枝操作,现在的条件是以前扩展过的条件的超集 + valid = True + for j in self.traversed: # 剪枝操作 + if j <= c_attr: + valid = False + if self.verbose: + print("———— --被剪枝") + break + + if valid: + # 把符合条件的动作节点都放到列表里 + if self.verbose: + print("———— -- %s 符合条件放入列表" % actions[i].name) + c_attr_node = Leaf(type='cond', content=c_attr, mincost=current_mincost + actions[i].cost) + a_attr_node = Leaf(type='act', content=actions[i], mincost=current_mincost + actions[i].cost) + cond_anc_pair = CondActPair(cond_leaf=c_attr_node, act_leaf=a_attr_node) + self.nodes.append(copy.deepcopy(cond_anc_pair)) # condition node list + self.traversed.append(c_attr) # 重点 the set of expanded conditions + + if self.verbose: + print("算法结束!\n") + return True + + def print_solution(self): + print("========= BT ==========") # 树的bfs遍历 + nodes_ls = [] + nodes_ls.append(self.bt) + while len(nodes_ls) != 0: + parnode = nodes_ls[0] + print("Parrent:", parnode.type) + for child in parnode.children: + if isinstance(child, Leaf): + print("---- Leaf:", child.content) + elif isinstance(child, ControlBT): + print("---- ControlBT:", child.type) + nodes_ls.append(child) + print() + nodes_ls.pop(0) + print("========= BT ==========\n") + + # 返回所有能到达目标状态的初始状态 + def get_all_state_leafs(self): + state_leafs=[] + + nodes_ls = [] + nodes_ls.append(self.bt) + while len(nodes_ls) != 0: + parnode = nodes_ls[0] + for child in parnode.children: + if isinstance(child, Leaf): + if child.type == "cond": + state_leafs.append(child.content) + elif isinstance(child, ControlBT): + nodes_ls.append(child) + nodes_ls.pop(0) + + return state_leafs + + + # 树的dfs + def dfs_ptml(self,parnode): + for child in parnode.children: + if isinstance(child, Leaf): + if child.type == 'cond': + self.ptml_string += "cond " + c_set_str = ', '.join(map(str, child.content)) + "\n" + self.ptml_string += c_set_str + elif child.type == 'act': + self.ptml_string += 'act '+child.content.name+"\n" + elif isinstance(child, ControlBT): + if parnode.type == '?': + self.ptml_string += "selector{\n" + self.dfs_ptml(parnode=child) + elif parnode.type == '>': + self.ptml_string += "sequence{\n" + self.dfs_ptml( parnode=child) + self.ptml_string += '}\n' + + + def get_ptml(self): + self.ptml_string = "selector{\n" + self.dfs_ptml(self.bt.children[0]) + self.ptml_string += '}\n' + return self.ptml_string + + + def save_ptml_file(self,file_name): + self.ptml_string = "selector{\n" + self.dfs_ptml(self.bt.children[0]) + self.ptml_string += '}\n' + with open(f'./{file_name}.ptml', 'w') as file: + file.write(self.ptml_string) + return self.ptml_string diff --git a/opt_bt_expansion/README.assets/image-20231103191141047.png b/opt_bt_expansion/README.assets/image-20231103191141047.png new file mode 100644 index 0000000000000000000000000000000000000000..6453647b76058878bfa6767963b630e8410b921c GIT binary patch literal 55821 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%0AKf%5_EYX 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 ZoE*=jX`)、动作节点(`act`)或条件节点(`cond`)。 +- 上述两个类都包含 `tick` 方法。 + +### 2. `OptimalBTExpansionAlgorithm.py` 实现最优行为树扩展算法 + +![image-20231103191141047](README.assets/image-20231103191141047.png) + +定义行动类 +```python +#定义行动类,行动包括前提、增加和删除影响 +class Action: + def __init__(self,name='anonymous action',pre=set(),add=set(),del_set=set(),cost=1): + self.pre=copy.deepcopy(pre) + self.add=copy.deepcopy(add) + self.del_set=copy.deepcopy(del_set) + self.name=name + self.cost=cost + + def __str__(self): + return self.name +``` + +调用算法 +```python +algo = OptBTExpAlgorithm(verbose=True) +algo.clear() +algo.run_algorithm(start, goal, actions) # 使用算法得到行为树在 algo.bt +algo.print_solution() # 打印行为树 +val, obj = algo.bt.tick(state) # 执行行为树 +algo.save_ptml_file("bt.ptml") # 保存行为树为 ptml 文件 +``` + +### 3. **`tools.py`** 实现打印数据、行为树测试等模块 + +使用方法 + +```python +print_action_data_table(goal,start,actions) # 打印所有变量 + +# 行为树鲁棒性测试,随机生成规划问题 +# 设置生成规划问题集的超参数:文字数、解深度、迭代次数 +seed=1 +literals_num=10 +depth = 10 +iters= 10 +BTTest(seed=seed,literals_num=literals_num,depth=depth,iters=iters) +``` + +### 4. `example.py` 中设计规划案例 goals, start,actions + +```python +def MoveBtoB (): + actions=[] + a = Action(name="Move(b,ab)") + a.pre={'Free(ab)','WayClear'} + a.add={'At(b,ab)'} + a.del_set= {'Free(ab)','At(b,pb)'} + a.cost = 1 + actions.append(a) + + a=Action(name="Move(s,ab)") + a.pre={'Free(ab)'} + a.add={'Free(ab)','WayClear'} + a.del_set={'Free(ab)','At(s,ps)'} + a.cost = 1 + actions.append(a) + + a=Action(name="Move(s,as)") + a.pre={'Free(as)'} + a.add={'At(s,ps)','WayClear'} + a.del_set={'Free(as)','At(s,ps)'} + a.cost = 1 + actions.append(a) + + start = {'Free(ab)','Free(as)','At(b,pb)','At(s,ps)'} + goal= {'At(b,ab)'} + return goal,start,actions +``` + +### 5. `opt_bt_exp_main.py` 为主函数,在此演示如何调用最优行为树扩展算法得到完全扩展最优行为树 +```python +actions=[ + Action(name='PutDown(Table,Coffee)', pre={'Holding(Coffee)','At(Robot,Table)'}, add={'At(Table,Coffee)','NotHolding'}, del_set={'Holding(Coffee)'}, cost=1) + ………… +] +algo = BTOptExpInterface(actions) + +goal = {'At(Table,Coffee)'} +ptml_string = algo.process(goal,start) +print(ptml_string) + +``` +两种检测方法,用于检测当前状态 `start` 能否到达目标状态 `goal` + +```python +# 判断初始状态能否到达目标状态 +start = {'At(Robot,Bar)', 'Holding(VacuumCup)', 'Available(Table)', 'Available(CoffeeMachine)','Available(FrontDesk)'} +# 方法一:算法返回所有可能的初始状态,在里面看看有没有对应的初始状态 +right_bt = algo.find_all_leaf_states_contain_start(start) +if not right_bt: + print("ERROR1: The current state cannot reach the goal state!") +else: + print("Right1: The current state can reach the goal state!") + + +# 方法二:预先跑一边行为树,看能否到达目标状态 +right_bt2 = algo.run_bt_from_start(goal,start) +if not right_bt2: + print("ERROR2: The current state cannot reach the goal state!") +else: + print("Right2: The current state can reach the goal state!") + +``` + diff --git a/opt_bt_expansion/examples.py b/opt_bt_expansion/examples.py new file mode 100644 index 0000000..743f554 --- /dev/null +++ b/opt_bt_expansion/examples.py @@ -0,0 +1,174 @@ + +from opt_bt_expansion.OptimalBTExpansionAlgorithm import Action + + + +def MakeCoffee(): + actions=[ + Action(name='Put(Table,Coffee)', pre={'Holding(Coffee)','At(Table)'}, add={'At(Table,Coffee)','NotHolding'}, del_set={'Holding(Coffee)'}, cost=1), + Action(name='Put(Table,VacuumCup)', pre={'Holding(VacuumCup)','At(Table)'}, add={'At(Table,VacuumCup)','NotHolding'}, del_set={'Holding(VacuumCup)'}, cost=1), + + Action(name='Grasp(Coffee)', pre={'NotHolding','At(Coffee)'}, add={'Holding(Coffee)'}, del_set={'NotHolding'}, cost=1), + + Action(name='MoveTo(Table)', pre={'Exist(Table)'}, add={'At(Table)'}, del_set={'At(FrontDesk)','At(Coffee)','At(CoffeeMachine)'}, cost=1), + Action(name='MoveTo(Coffee)', pre={'Exist(Coffee)'}, add={'At(Coffee)'}, del_set={'At(FrontDesk)','At(Table)','At(CoffeeMachine)'}, cost=1), + Action(name='MoveTo(CoffeeMachine)', pre={'Exist(CoffeeMachine)'}, add={'At(CoffeeMachine)'}, del_set={'At(FrontDesk)','At(Coffee)','At(Table)'}, cost=1), + + Action(name='OpCoffeeMachine', pre={'At(CoffeeMachine)','NotHolding'}, add={'Exist(Coffee)','At(Coffee)'}, del_set=set(), cost=1), + ] + + start = {'At(FrontDesk)','Holding(VacuumCup)','Exist(Table)','Exist(CoffeeMachine)','Exist(FrontDesk)'} + goal = {'At(Table,Coffee)'} + return goal,start,actions + +# 本例子中,将 VacuumCup 放到 FrontDesk,比 MoveTo(Table) 再 Put(Table,VacuumCup) 的 cost 要小 +def MakeCoffeeCost(): + actions=[ + Action(name='PutDown(Table,Coffee)', pre={'Holding(Coffee)','At(Robot,Table)'}, add={'At(Table,Coffee)','NotHolding'}, del_set={'Holding(Coffee)'}, cost=1), + Action(name='PutDown(Table,VacuumCup)', pre={'Holding(VacuumCup)','At(Robot,Table)'}, add={'At(Table,VacuumCup)','NotHolding'}, del_set={'Holding(VacuumCup)'}, cost=1), + + Action(name='PickUp(Coffee)', pre={'NotHolding','At(Robot,Coffee)'}, add={'Holding(Coffee)'}, del_set={'NotHolding'}, cost=1), + + Action(name='MoveTo(Table)', pre={'Available(Table)'}, add={'At(Robot,Table)'}, del_set={'At(Robot,FrontDesk)','At(Robot,Coffee)','At(Robot,CoffeeMachine)'}, cost=1), + Action(name='MoveTo(Coffee)', pre={'Available(Coffee)'}, add={'At(Robot,Coffee)'}, del_set={'At(Robot,FrontDesk)','At(Robot,Table)','At(Robot,CoffeeMachine)'}, cost=1), + Action(name='MoveTo(CoffeeMachine)', pre={'Available(CoffeeMachine)'}, add={'At(Robot,CoffeeMachine)'}, del_set={'At(Robot,FrontDesk)','At(Robot,Coffee)','At(Robot,Table)'}, cost=1), + + Action(name='OpCoffeeMachine', pre={'At(Robot,CoffeeMachine)','NotHolding'}, add={'Available(Coffee)','At(Robot,Coffee)'}, del_set=set(), cost=1), + ] + + start = {'At(Robot,Bar)','Holding(VacuumCup)','Available(Table)','Available(CoffeeMachine)','Available(FrontDesk)'} + goal = {'At(Table,Coffee)'} + + return goal,start,actions + + +# test +def Test(): + actions=[ + Action(name='a1', pre={6}, add={0,2,4}, del_set={1,5}, cost=1), + Action(name='a2', pre=set(), add={0,1}, del_set=set(), cost=1), + Action(name='a3', pre={1,6}, add={0,2,3,5}, del_set={1,6}, cost=1), + Action(name='a4', pre={0,2,3}, add={4,5}, del_set={0,6}, cost=1), + Action(name='a5', pre={0,1,4}, add={2,3,6}, del_set={0}, cost=1), + ] + + start = {1,2,6} + goal={0,1,2,4,6} + return goal,start,actions + +# def Test(): +# actions=[ +# Action(name='a1', pre={2}, add={1}, del_set=set(), cost=1), +# Action(name='a2', pre=set(), add={1}, del_set={0,2}, cost=1), +# Action(name='a3', pre={1}, add=set(), del_set={0,2}, cost=1), +# Action(name='a4', pre=set(), add={0}, del_set=set(), cost=1), +# Action(name='a5', pre={1}, add={0,2}, del_set={1}, cost=1), +# Action(name='a6', pre={1}, add=set(), del_set={0,1,2}, cost=1), +# Action(name='a7', pre={1}, add={2}, del_set={0, 2}, cost=1), +# ] +# +# start = {1,2} +# goal={0,1} +# return goal,start,actions + + +# todo: 最原始的例子 +def MoveBtoB_num (): + actions=[] + a = Action(name='a1') + a.pre={1,4} + a.add={"c_goal"} + a.del_set={1,4} + a.cost = 1 + actions.append(a) + + a=Action(name='a2') + a.pre={1,2,3} + a.add={"c_goal"} + a.del_set={1,2,3} + a.cost = 1 + actions.append(a) + + a=Action(name='a3') + a.pre={1,2} + a.add={4} + a.del_set={2} + a.cost = 1 + actions.append(a) + + a=Action(name='a4') + a.pre={"c_start"} + a.add={1,2,3} + a.del_set={"c_start",4} + a.cost = 1 + actions.append(a) + + start = {"c_start"} + goal={"c_goal"} + return goal,start,actions + + +# todo: 最原始的例子 +def MoveBtoB (): + actions=[] + a = Action(name="Move(b,ab)") #'movebtob' + a.pre={'Free(ab)','WayClear'} #{1,2} + a.add={'At(b,ab)'} #{3} + a.del_set= {'Free(ab)','At(b,pb)'} #{1,4} + a.cost = 1 + actions.append(a) + + a=Action(name="Move(s,ab)") #'moveatob' + a.pre={'Free(ab)'} #{1} + a.add={'Free(ab)','WayClear'} #{5,2} + a.del_set={'Free(ab)','At(s,ps)'} #{1,6} + a.cost = 1 + actions.append(a) + + a=Action(name="Move(s,as)") #'moveatoa' + a.pre={'Free(as)'} #{7} + a.add={'At(s,ps)','WayClear'} #{8,2} + a.del_set={'Free(as)','At(s,ps)'} #{7,6} + a.cost = 1 + actions.append(a) + + start = {'Free(ab)','Free(as)','At(b,pb)','At(s,ps)'} #{1,7,4,6} + goal= {'At(b,ab)'} #{3} + return goal,start,actions + + +# 小蔡师兄论文里的例子 +def Cond2BelongsToCond3(): + actions=[] + a = Action(name='a1') + a.pre={1,4} + a.add={"c_goal"} + a.del_set={1,4} + a.cost = 1 + actions.append(a) + + a=Action(name='a2') + a.pre={1,2,3} + a.add={"c_goal"} + a.del_set={1,2,3} + a.cost = 100 + actions.append(a) + + a=Action(name='a3') + a.pre={1,2} + a.add={4} + a.del_set={2} + a.cost = 1 + actions.append(a) + + a=Action(name='a4') + a.pre={"c_start"} + a.add={1,2,3} + a.del_set={"c_start",4} + a.cost = 1 + actions.append(a) + + start = {"c_start"} + goal={"c_goal"} + return goal,start,actions + diff --git a/opt_bt_expansion/opt_bt_exp_main.py b/opt_bt_expansion/opt_bt_exp_main.py new file mode 100644 index 0000000..5047370 --- /dev/null +++ b/opt_bt_expansion/opt_bt_exp_main.py @@ -0,0 +1,121 @@ +from opt_bt_expansion.BehaviorTree import Leaf,ControlBT # 行为结点类:叶子结点和非叶子节点 +from opt_bt_expansion.OptimalBTExpansionAlgorithm import Action,OptBTExpAlgorithm,state_transition # 调用最优行为树扩展算法 +from opt_bt_expansion.tools import print_action_data_table,BTTest +from opt_bt_expansion.examples import MoveBtoB_num,MoveBtoB,Cond2BelongsToCond3 # 导入三个例子 +from opt_bt_expansion.examples import * + + +# 封装好的主接口 +class BTOptExpInterface: + def __init__(self, action_list): + """ + Initialize the BTOptExpansion with a list of actions. + :param action_list: A list of actions to be used in the behavior tree. + """ + # self.actions = [] + # for act in action_list: + # a = Action(name=act.name) + # a.pre=act['pre'] + # a.add=act['add'] + # a.del_set= act['del_set'] + # a.cost = 1 + # self.actions.append(a) + self.actions = action_list + self.has_processed = False + + def process(self, goal): + """ + Process the input sets and return a string result. + :param input_set: The set of goal states and the set of initial states. + :return: A PTML string representing the outcome of the behavior tree. + """ + self.goal = goal + self.algo = OptBTExpAlgorithm(verbose=False) + self.algo.clear() + self.algo.run_algorithm(self.goal, self.actions) # 调用算法得到行为树保存至 algo.bt + self.ptml_string = self.algo.get_ptml() + self.has_processed = True + # algo.print_solution() # print behavior tree + + return self.ptml_string + + # 方法一:查找所有初始状态是否包含当前状态 + def find_all_leaf_states_contain_start(self,start): + if not self.has_processed: + raise RuntimeError("The process method must be called before find_all_leaf_states_contain_start!") + # 返回所有能到达目标状态的初始状态 + state_leafs = self.algo.get_all_state_leafs() + for state in state_leafs: + if start >= state: + return True + return False + + # 方法二:模拟跑一遍行为树,看 start 能够通过执行一系列动作到达 goal + def run_bt_from_start(self,goal,start): + if not self.has_processed: + raise RuntimeError("The process method must be called before run_bt_from_start!") + # 检查是否能到达目标 + right_bt = True + state = start + steps = 0 + val, obj = self.algo.bt.tick(state) + while val != 'success' and val != 'failure': + state = state_transition(state, obj) + val, obj = self.algo.bt.tick(state) + if (val == 'failure'): + # print("bt fails at step", steps) + right_bt = False + steps += 1 + if not goal <= state: + # print("wrong solution", steps) + right_bt = False + else: + pass + # print("right solution", steps) + return right_bt + + + + +if __name__ == '__main__' : + + # todo: Example Cafe + # todo: Define goal, start, actions + actions=[ + Action(name='PutDown(Table,Coffee)', pre={'Holding(Coffee)','At(Robot,Table)'}, add={'At(Table,Coffee)','NotHolding'}, del_set={'Holding(Coffee)'}, cost=1), + Action(name='PutDown(Table,VacuumCup)', pre={'Holding(VacuumCup)','At(Robot,Table)'}, add={'At(Table,VacuumCup)','NotHolding'}, del_set={'Holding(VacuumCup)'}, cost=1), + + Action(name='PickUp(Coffee)', pre={'NotHolding','At(Robot,Coffee)'}, add={'Holding(Coffee)'}, del_set={'NotHolding'}, cost=1), + + Action(name='MoveTo(Table)', pre={'Available(Table)'}, add={'At(Robot,Table)'}, del_set={'At(Robot,FrontDesk)','At(Robot,Coffee)','At(Robot,CoffeeMachine)'}, cost=1), + Action(name='MoveTo(Coffee)', pre={'Available(Coffee)'}, add={'At(Robot,Coffee)'}, del_set={'At(Robot,FrontDesk)','At(Robot,Table)','At(Robot,CoffeeMachine)'}, cost=1), + Action(name='MoveTo(CoffeeMachine)', pre={'Available(CoffeeMachine)'}, add={'At(Robot,CoffeeMachine)'}, del_set={'At(Robot,FrontDesk)','At(Robot,Coffee)','At(Robot,Table)'}, cost=1), + + Action(name='OpCoffeeMachine', pre={'At(Robot,CoffeeMachine)','NotHolding'}, add={'Available(Coffee)','At(Robot,Coffee)'}, del_set=set(), cost=1), + ] + algo = BTOptExpInterface(actions) + + + goal = {'At(Table,Coffee)'} + ptml_string = algo.process(goal) + print(ptml_string) + + file_name = "MakeCoffee" + with open(f'./{file_name}.ptml', 'w') as file: + file.write(ptml_string) + + + # 判断初始状态能否到达目标状态 + start = {'At(Robot,Bar)', 'Holding(VacuumCup)', 'Available(Table)', 'Available(CoffeeMachine)','Available(FrontDesk)'} + # 方法一:算法返回所有可能的初始状态,在里面看看有没有对应的初始状态 + right_bt = algo.find_all_leaf_states_contain_start(start) + if not right_bt: + print("ERROR1: The current state cannot reach the goal state!") + else: + print("Right1: The current state can reach the goal state!") + # 方法二:预先跑一边行为树,看能否到达目标状态 + right_bt2 = algo.run_bt_from_start(goal,start) + if not right_bt2: + print("ERROR2: The current state cannot reach the goal state!") + else: + print("Right2: The current state can reach the goal state!") diff --git a/opt_bt_expansion/tools.py b/opt_bt_expansion/tools.py new file mode 100644 index 0000000..cf84229 --- /dev/null +++ b/opt_bt_expansion/tools.py @@ -0,0 +1,167 @@ + + +from tabulate import tabulate +import numpy as np +import random +from opt_bt_expansion.OptimalBTExpansionAlgorithm import Action,OptBTExpAlgorithm +import time + + +def print_action_data_table(goal,start,actions): + data = [] + for a in actions: + data.append([a.name ,a.pre ,a.add ,a.del_set ,a.cost]) + data.append(["Goal" ,goal ," " ,"Start" ,start]) + print(tabulate(data, headers=["Name", "Pre", "Add" ,"Del" ,"Cost"], tablefmt="fancy_grid")) # grid plain simple github fancy_grid + + +# 从状态随机生成一个行动 +def generate_from_state(act,state,num): + for i in range(0,num): + if i in state: + if random.random() >0.5: + act.pre.add(i) + if random.random() >0.5: + act.del_set.add(i) + continue + if random.random() > 0.5: + act.add.add(i) + continue + if random.random() >0.5: + act.del_set.add(i) + +def print_action(act): + print (act.pre) + print(act.add) + print(act.del_set) + + + +#行为树测试代码 +def BTTest(seed=1,literals_num=10,depth=10,iters=10,total_count=1000): + print("============= BT Test ==============") + random.seed(seed) + # 设置生成规划问题集的超参数:文字数、解深度、迭代次数 + literals_num=literals_num + depth = depth + iters= iters + total_tree_size = [] + total_action_num = [] + total_state_num = [] + total_steps_num=[] + #fail_count=0 + #danger_count=0 + success_count =0 + failure_count = 0 + planning_time_total = 0.0 + # 实验1000次 + for count in range (total_count): + + action_num = 1 + + # 生成一个规划问题,包括随机的状态和行动,以及目标状态 + states = [] + actions = [] + start = generate_random_state(literals_num) + state = start + states.append(state) + #print (state) + for i in range (0,depth): + a = Action() + generate_from_state(a,state,literals_num) + if not a in actions: + a.name = "a"+str(action_num) + action_num+=1 + actions.append(a) + state = state_transition(state,a) + if state in states: + pass + else: + states.append(state) + #print(state) + + goal = states[-1] + state = start + for i in range (0,iters): + a = Action() + generate_from_state(a,state,literals_num) + if not a in actions: + a.name = "a"+str(action_num) + action_num+=1 + actions.append(a) + state = state_transition(state,a) + if state in states: + pass + else: + states.append(state) + state = random.sample(states,1)[0] + + # 选择测试本文算法btalgorithm,或对比算法weakalgorithm + algo = OptBTExpAlgorithm() + #algo = Weakalgorithm() + start_time = time.time() + # print_action_data_table(goal, start, list(actions)) + if algo.run_algorithm(start, goal, actions):#运行算法,规划后行为树为algo.bt + total_tree_size.append( algo.bt.count_size()-1) + # algo.print_solution() # 打印行为树 + else: + print ("error") + end_time = time.time() + planning_time_total += (end_time-start_time) + + #开始从初始状态运行行为树,测试 + state=start + steps=0 + val, obj = algo.bt.tick(state)#tick行为树,obj为所运行的行动 + while val !='success' and val !='failure':#运行直到行为树成功或失败 + state = state_transition(state,obj) + val, obj = algo.bt.tick(state) + if(val == 'failure'): + print("bt fails at step",steps) + steps+=1 + if(steps>=500):#至多运行500步 + break + if not goal <= state:#错误解,目标条件不在执行后状态满足 + #print ("wrong solution",steps) + failure_count+=1 + + else:#正确解,满足目标条件 + #print ("right solution",steps) + success_count+=1 + total_steps_num.append(steps) + algo.clear() + total_action_num.append(len(actions)) + total_state_num.append(len(states)) + + print ("success:",success_count,"failure:",failure_count)#算法成功和失败次数 + print("Total Tree Size: mean=",np.mean(total_tree_size), "std=",np.std(total_tree_size, ddof=1))#1000次测试树大小 + print ("Total Steps Num: mean=",np.mean(total_steps_num),"std=",np.std(total_steps_num,ddof=1)) + print ("Average number of states:",np.mean(total_state_num))#1000次问题的平均状态数 + print ("Average number of actions",np.mean(total_action_num))#1000次问题的平均行动数 + print("Planning Time Total:",planning_time_total,planning_time_total/1000.0) + print("============ End BT Test ===========") + + # xiao cai + # success: 1000 failure: 0 + # Total Tree Size: mean= 35.303 std= 29.71336526001515 + # Total Steps Num: mean= 1.898 std= 0.970844240101644 + # Average number of states: 20.678 + # Average number of actions 20.0 + # Planning Time Total: 0.6280641555786133 0.0006280641555786133 + + # our start + # success: 1000 failure: 0 + # Total Tree Size: mean= 17.945 std= 12.841997192488865 + # Total Steps Num: mean= 1.785 std= 0.8120556843187752 + # Average number of states: 20.678 + # Average number of actions 20.0 + # Planning Time Total: 1.4748523235321045 0.0014748523235321046 + + # our + # success: 1000 failure: 0 + # Total Tree Size: mean= 48.764 std= 20.503626574406358 + # Total Steps Num: mean= 1.785 std= 0.8120556843187752 + # Average number of states: 20.678 + # Average number of actions 20.0 + # Planning Time Total: 3.3271877765655518 0.0033271877765655516 +