From 95b540f6703dab127b2424edc60d5d1e4e8f3766 Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 10 Sep 2024 16:25:39 -0400 Subject: [PATCH 1/7] initial action_library integration --- src/social_norms_trees/action_library.py | 119 +++++++++++++++++++++++ src/social_norms_trees/ui_wrapper.py | 99 +++++++++++-------- 2 files changed, 180 insertions(+), 38 deletions(-) create mode 100644 src/social_norms_trees/action_library.py diff --git a/src/social_norms_trees/action_library.py b/src/social_norms_trees/action_library.py new file mode 100644 index 00000000..aecd2d8c --- /dev/null +++ b/src/social_norms_trees/action_library.py @@ -0,0 +1,119 @@ +class ActionLibrary: + def __init__(self): + self.actions = { + "Action A": { + "id": "001", + "nickname": "Action A", + "type": "Dummy", + }, + "Action B": { + "id": "002", + "nickname": "Action B", + "type": "Dummy", + }, + "Action C": { + "id": "003", + "nickname": "Action C", + "type": "Dummy", + }, + "Action D": { + "id": "004", + "nickname": "Action D", + "type": "Dummy", + }, + "Action E": { + "id": "005", + "nickname": "Action E", + "type": "Dummy", + }, + "Action F": { + "id": "006", + "nickname": "Action F", + "type": "Dummy", + }, + "Action G": { + "id": "007", + "nickname": "Action G", + "type": "Dummy", + }, + "Action H": { + "id": "008", + "nickname": "Action H", + "type": "Dummy", + }, + + "Action I": { + "id": "009", + "nickname": "Action I", + "type": "Dummy", + }, + "Action J": { + "id": "010", + "nickname": "Action J", + "type": "Dummy", + }, + "Action K": { + "id": "011", + "nickname": "Action K", + "type": "Dummy", + }, + } + + def get_action_by_nickname(self, nickname): + return self.actions.get(nickname) + + def get_action_by_id(self, id): + for action in self.actions.values(): + if action.id == id: + return action + return None + + + +# "base_behavior_tree": { +# "name": "Sequence A", +# "children": [ +# { +# "name": "Action A", +# "children": [] +# }, +# { +# "name": "Action B", +# "children": [] +# }, +# { +# "name": "Sequence B", +# "children": [ +# { +# "name": "Action C", +# "children": [] +# }, +# { +# "name": "Action D", +# "children": [] +# } +# ] +# }, +# { +# "name": "Sequence C", +# "children": [ +# { +# "name": "Action E", +# "children": [] +# }, +# { +# "name": "Action F", +# "children": [] +# }, +# { +# "name": "Action G", +# "children": [] +# }, +# { +# "name": "Action H", +# "children": [] +# } +# ] +# } +# ] +# }, \ No newline at end of file diff --git a/src/social_norms_trees/ui_wrapper.py b/src/social_norms_trees/ui_wrapper.py index 4a56b3d5..27b0aea0 100644 --- a/src/social_norms_trees/ui_wrapper.py +++ b/src/social_norms_trees/ui_wrapper.py @@ -34,7 +34,9 @@ def experiment_setup(db): print("\n") origin_tree = load_behavior_tree() - + print(origin_tree) + + print("\n") experiment_id = initialize_experiment_record(db, participant_id, origin_tree) print("\nSetup Complete.\n") @@ -49,45 +51,68 @@ def participant_login(): return participant_id -def get_behavior_trees(): - #TODO: Get behavior trees from respective data structure - - print("1. Original Tree") - return [ - py_trees.composites.Sequence( - "", - False, - children=[ - py_trees.behaviours.Dummy(), - py_trees.behaviours.Dummy(), - py_trees.composites.Sequence( - "", - False, - children=[ - py_trees.behaviours.Success(), - py_trees.behaviours.Dummy(), - ], - ), - py_trees.composites.Sequence( - "", - False, - children=[ - py_trees.behaviours.Dummy(), - py_trees.behaviours.Failure(), - py_trees.behaviours.Dummy(), - py_trees.behaviours.Running(), - ], - ), - ], - ) - ] +# def get_behavior_trees(): +# #TODO: Get behavior trees from respective data structure + +# print("1. Original Tree") + +behavior_trees = { + "tree_1": { + "name": "Sequence A", + "children": [ + { + "name": "Action A", + "children": [] + }, + { + "name": "Action B", + "children": [] + }, + { + "name": "Sequence B", + "children": [ + { + "name": "Action C", + "children": [] + }, + { + "name": "Action D", + "children": [] + } + ] + }, + { + "name": "Sequence C", + "children": [ + { + "name": "Action E", + "children": [] + }, + { + "name": "Action F", + "children": [] + }, + { + "name": "Action G", + "children": [] + }, + { + "name": "Action H", + "children": [] + } + ] + } + ] + }, +} def load_behavior_tree(): - - tree_array = get_behavior_trees() + for idx, tree_name in enumerate(behavior_trees.keys(), 1): + print(f"{idx}. {tree_name}") + tree_index = click.prompt("Please select a behavior tree to load for the experiment (enter the number)", type=int) - return tree_array[tree_index - 1] + return behavior_trees[f"tree_{tree_index}"] def initialize_experiment_record(db, participant_id, origin_tree): @@ -96,8 +121,6 @@ def initialize_experiment_record(db, participant_id, origin_tree): #TODO: look into python data class - #TODO: flatten structure of db to simply collction of experiment runs, that will include a field for the participant_id - #instead of grouping by participants experiment_record = { "experiment_id": experiment_id, "participant_id": participant_id, From e72a97bbbf4b2f82458971fa18362a5f19226ced Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 11 Sep 2024 11:34:15 -0400 Subject: [PATCH 2/7] adding more info to each action --- src/social_norms_trees/custom_node_library.py | 7 +++ src/social_norms_trees/mutate_tree.py | 19 ++++--- src/social_norms_trees/serialize_tree.py | 23 ++++---- src/social_norms_trees/ui_wrapper.py | 52 +++++++++++++------ 4 files changed, 69 insertions(+), 32 deletions(-) create mode 100644 src/social_norms_trees/custom_node_library.py diff --git a/src/social_norms_trees/custom_node_library.py b/src/social_norms_trees/custom_node_library.py new file mode 100644 index 00000000..40f7dd99 --- /dev/null +++ b/src/social_norms_trees/custom_node_library.py @@ -0,0 +1,7 @@ +import py_trees + +class CustomAction(py_trees.behaviours.Dummy): + def __init__(self, name, id, nickname): + super().__init__(name) + self.id = id + self.nickname = nickname \ No newline at end of file diff --git a/src/social_norms_trees/mutate_tree.py b/src/social_norms_trees/mutate_tree.py index dd12024d..6e128a76 100644 --- a/src/social_norms_trees/mutate_tree.py +++ b/src/social_norms_trees/mutate_tree.py @@ -337,12 +337,15 @@ def remove_node(tree: T, node: Optional[py_trees.behaviour.Behaviour] = None) -> warnings.warn( f"{node}'s parent is None, so we can't remove it. You can't remove the root node." ) - return tree + action_log = "no node removed" + return tree, action_log elif isinstance(parent_node, py_trees.composites.Composite): parent_node.remove_child(node) + action_log = f"node {node.id} removed" else: raise NotImplementedError() - return tree + + return tree, action_log def move_node( @@ -373,7 +376,9 @@ def move_node( node.parent.remove_child(node) new_parent.insert_child(node, index) - return tree + + action_log = f"node {node.id} moved" + return tree, action_log def exchange_nodes( @@ -435,7 +440,9 @@ def exchange_nodes( node0_parent, node0_index = node0.parent, node0.parent.children.index(node0) node1_parent, node1_index = node1.parent, node1.parent.children.index(node1) - tree = move_node(tree, node0, node1_parent, node1_index) - tree = move_node(tree, node1, node0_parent, node0_index) + tree, _ = move_node(tree, node0, node1_parent, node1_index) + tree, _ = move_node(tree, node1, node0_parent, node0_index) + + action_log = f"node {node0.id} and node {node1.id} exchanged" - return tree \ No newline at end of file + return tree, action_log \ No newline at end of file diff --git a/src/social_norms_trees/serialize_tree.py b/src/social_norms_trees/serialize_tree.py index 2db28d69..ac6e1cbe 100644 --- a/src/social_norms_trees/serialize_tree.py +++ b/src/social_norms_trees/serialize_tree.py @@ -1,4 +1,5 @@ import py_trees +from social_norms_trees.custom_node_library import CustomAction def serialize_tree(tree): @@ -12,20 +13,22 @@ def serialize_node(node): return serialize_node(tree) -def deserialize_tree(tree): +def deserialize_tree(tree, action_library): def deserialize_node(node): node_type = node['type'] children = [deserialize_node(child) for child in node['children']] if node_type == 'Sequence': return py_trees.composites.Sequence(node['name'], False, children=children) - elif node_type == 'Dummy': - return py_trees.behaviours.Dummy(node['name']) - elif node_type == 'Success': - return py_trees.behaviours.Success(node['name']) - elif node_type == 'Failure': - return py_trees.behaviours.Failure(node['name']) - elif node_type == 'Running': - return py_trees.behaviours.Running(node['name']) - + elif node_type == 'Action': + action = action_library.get_action_by_nickname(node['name']) + if action: + return CustomAction( + name=action['nickname'], + id=action['id'], + nickname=action['nickname'] + ) + else: + raise ValueError(f"Action {node['name']} not found in action library") + return deserialize_node(tree) \ No newline at end of file diff --git a/src/social_norms_trees/ui_wrapper.py b/src/social_norms_trees/ui_wrapper.py index 27b0aea0..0ee04d38 100644 --- a/src/social_norms_trees/ui_wrapper.py +++ b/src/social_norms_trees/ui_wrapper.py @@ -9,6 +9,8 @@ from social_norms_trees.mutate_tree import move_node, exchange_nodes, remove_node from social_norms_trees.serialize_tree import serialize_tree, deserialize_tree +from social_norms_trees.action_library import ActionLibrary + DB_FILE = "db.json" @@ -27,16 +29,14 @@ def save_db(db, db_file): with open(db_file, "w") as f: json.dump(db, f, indent=4) -def experiment_setup(db): +def experiment_setup(db, action_library): print("\n") participant_id = participant_login() print("\n") - origin_tree = load_behavior_tree() - print(origin_tree) - - print("\n") + origin_tree = load_behavior_tree(action_library) + experiment_id = initialize_experiment_record(db, participant_id, origin_tree) print("\nSetup Complete.\n") @@ -59,45 +59,56 @@ def participant_login(): behavior_trees = { "tree_1": { "name": "Sequence A", + "type": "Sequence", "children": [ { "name": "Action A", + "type": "Action", "children": [] }, { "name": "Action B", + "type": "Action", "children": [] }, { "name": "Sequence B", + "type": "Sequence", "children": [ { "name": "Action C", + "type": "Action", "children": [] }, { "name": "Action D", + "type": "Action", "children": [] } ] }, { "name": "Sequence C", + "type": "Sequence", "children": [ { "name": "Action E", + "type": "Action", "children": [] }, { "name": "Action F", + "type": "Action", "children": [] }, { "name": "Action G", + "type": "Action", "children": [] }, { "name": "Action H", + "type": "Action", "children": [] } ] @@ -107,12 +118,17 @@ def participant_login(): } -def load_behavior_tree(): +def load_behavior_tree(action_library): for idx, tree_name in enumerate(behavior_trees.keys(), 1): print(f"{idx}. {tree_name}") - tree_index = click.prompt("Please select a behavior tree to load for the experiment (enter the number)", type=int) - return behavior_trees[f"tree_{tree_index}"] + selected_index = click.prompt("Please select a behavior tree to load for the experiment (enter the number)", type=int) + + tree_config = behavior_trees[f"tree_{selected_index}"] + + experiment_ready_tree = deserialize_tree(tree_config, action_library) + + return experiment_ready_tree def initialize_experiment_record(db, participant_id, origin_tree): @@ -126,7 +142,7 @@ def initialize_experiment_record(db, participant_id, origin_tree): "participant_id": participant_id, "base_behavior_tree": serialize_tree(origin_tree), "start_date": datetime.now().isoformat(), - "actions": [], + "action_history": [], } db[experiment_id] = experiment_record @@ -159,14 +175,16 @@ def run_experiment(db, participant_id, origin_tree, experiment_id): ) if action == "1": - db[experiment_id]["actions"].append("move node") - move_node(origin_tree) + origin_tree, action_log = move_node(origin_tree) + db[experiment_id]["action_history"].append(action_log) elif action == "2": - db[experiment_id]["actions"].append("exchange node") - exchange_nodes(origin_tree) + origin_tree, action_log = exchange_nodes(origin_tree) + db[experiment_id]["action_history"].append(action_log) + elif action == "3": - db[experiment_id]["actions"].append("remove node") - remove_node(origin_tree) + origin_tree, action_log = remove_node(origin_tree) + db[experiment_id]["action_history"].append(action_log) + else: print("Invalid choice, please select a valid number (1, 2, or 3).\n") @@ -182,11 +200,13 @@ def run_experiment(db, participant_id, origin_tree, experiment_id): def main(): print("AIT Prototype #1 Simulator") + action_library = ActionLibrary() + DB_FILE = "db.json" db = load_db(DB_FILE) - participant_id, origin_tree, experiment_id = experiment_setup(db) + participant_id, origin_tree, experiment_id = experiment_setup(db, action_library) db = run_experiment(db, participant_id, origin_tree, experiment_id) save_db(db, DB_FILE) From 6e15cae068a4f88db068180d9ac2fcbedc4f6dc7 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 11 Sep 2024 11:35:18 -0400 Subject: [PATCH 3/7] remove comment --- src/social_norms_trees/action_library.py | 52 +----------------------- 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/src/social_norms_trees/action_library.py b/src/social_norms_trees/action_library.py index aecd2d8c..078fc008 100644 --- a/src/social_norms_trees/action_library.py +++ b/src/social_norms_trees/action_library.py @@ -66,54 +66,4 @@ def get_action_by_id(self, id): for action in self.actions.values(): if action.id == id: return action - return None - - - -# "base_behavior_tree": { -# "name": "Sequence A", -# "children": [ -# { -# "name": "Action A", -# "children": [] -# }, -# { -# "name": "Action B", -# "children": [] -# }, -# { -# "name": "Sequence B", -# "children": [ -# { -# "name": "Action C", -# "children": [] -# }, -# { -# "name": "Action D", -# "children": [] -# } -# ] -# }, -# { -# "name": "Sequence C", -# "children": [ -# { -# "name": "Action E", -# "children": [] -# }, -# { -# "name": "Action F", -# "children": [] -# }, -# { -# "name": "Action G", -# "children": [] -# }, -# { -# "name": "Action H", -# "children": [] -# } -# ] -# } -# ] -# }, \ No newline at end of file + return None \ No newline at end of file From b741009b4cac1e653e39a0f33fabc15e36ce0c6c Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 11 Sep 2024 11:50:40 -0400 Subject: [PATCH 4/7] update action_log object --- src/social_norms_trees/mutate_tree.py | 45 ++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/src/social_norms_trees/mutate_tree.py b/src/social_norms_trees/mutate_tree.py index 6e128a76..bd31284c 100644 --- a/src/social_norms_trees/mutate_tree.py +++ b/src/social_norms_trees/mutate_tree.py @@ -8,6 +8,8 @@ import click import py_trees +from datetime import datetime + T = TypeVar("T", bound=py_trees.behaviour.Behaviour) @@ -337,11 +339,20 @@ def remove_node(tree: T, node: Optional[py_trees.behaviour.Behaviour] = None) -> warnings.warn( f"{node}'s parent is None, so we can't remove it. You can't remove the root node." ) - action_log = "no node removed" - return tree, action_log + action_log = {} + return tree, elif isinstance(parent_node, py_trees.composites.Composite): parent_node.remove_child(node) - action_log = f"node {node.id} removed" + action_log = { + "type": "remove_node", + "nodes": [ + { + "id": node.id, + "nickname": node.nickname + }, + ], + "timestamp": datetime.now().isoformat(), + } else: raise NotImplementedError() @@ -376,8 +387,16 @@ def move_node( node.parent.remove_child(node) new_parent.insert_child(node, index) - - action_log = f"node {node.id} moved" + action_log = { + "type": "move_node", + "nodes": [ + { + "id": node.id, + "nickname": node.nickname + }, + ], + "timestamp": datetime.now().isoformat(), + } return tree, action_log @@ -443,6 +462,18 @@ def exchange_nodes( tree, _ = move_node(tree, node0, node1_parent, node1_index) tree, _ = move_node(tree, node1, node0_parent, node0_index) - action_log = f"node {node0.id} and node {node1.id} exchanged" - + action_log = { + "type": "exchange_nodes", + "nodes": [ + { + "id": node0.id, + "nickname": node0.nickname + }, + { + "id": node1.id, + "nickname": node1.nickname + }, + ], + "timestamp": datetime.now().isoformat(), + } return tree, action_log \ No newline at end of file From 982785ceca026ce630aadd4cab250d2e4f1e0625 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 11 Sep 2024 12:04:33 -0400 Subject: [PATCH 5/7] add more info to move_node action log --- src/social_norms_trees/mutate_tree.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/social_norms_trees/mutate_tree.py b/src/social_norms_trees/mutate_tree.py index bd31284c..2129e1a7 100644 --- a/src/social_norms_trees/mutate_tree.py +++ b/src/social_norms_trees/mutate_tree.py @@ -384,6 +384,7 @@ def move_node( assert isinstance(new_parent, py_trees.composites.Composite) assert isinstance(node.parent, py_trees.composites.Composite) + old_parent = node.parent.name node.parent.remove_child(node) new_parent.insert_child(node, index) @@ -392,7 +393,9 @@ def move_node( "nodes": [ { "id": node.id, - "nickname": node.nickname + "nickname": node.nickname, + "old parent": old_parent, + "new parent": node.parent.name }, ], "timestamp": datetime.now().isoformat(), From 268a54921d50f2947dd0c8d6f4e4c1700ee00b12 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 12 Sep 2024 16:05:20 -0400 Subject: [PATCH 6/7] update action library to behavior library, and fix action_log for all actions --- src/social_norms_trees/action_library.py | 64 ++++++++-------- src/social_norms_trees/custom_node_library.py | 14 +++- src/social_norms_trees/mutate_tree.py | 73 ++++++++++++------- src/social_norms_trees/serialize_tree.py | 20 ++--- src/social_norms_trees/ui_wrapper.py | 55 +++++++------- 5 files changed, 131 insertions(+), 95 deletions(-) diff --git a/src/social_norms_trees/action_library.py b/src/social_norms_trees/action_library.py index 078fc008..3e1f29b6 100644 --- a/src/social_norms_trees/action_library.py +++ b/src/social_norms_trees/action_library.py @@ -1,69 +1,73 @@ -class ActionLibrary: +class BehaviorLibrary: def __init__(self): - self.actions = { - "Action A": { + self.behaviors = { + "Behavior A": { "id": "001", - "nickname": "Action A", + "nickname": "Behavior A", "type": "Dummy", }, - "Action B": { + "Behavior B": { "id": "002", - "nickname": "Action B", + "nickname": "Behavior B", "type": "Dummy", }, - "Action C": { + "Behavior C": { "id": "003", - "nickname": "Action C", + "nickname": "Behavior C", "type": "Dummy", }, - "Action D": { + "Behavior D": { "id": "004", - "nickname": "Action D", + "nickname": "Behavior D", "type": "Dummy", }, - "Action E": { + "Behavior E": { "id": "005", - "nickname": "Action E", + "nickname": "Behavior E", "type": "Dummy", }, - "Action F": { + "Behavior F": { "id": "006", - "nickname": "Action F", + "nickname": "Behavior F", "type": "Dummy", }, - "Action G": { + "Behavior G": { "id": "007", - "nickname": "Action G", + "nickname": "Behavior G", "type": "Dummy", }, - "Action H": { + "Behavior H": { "id": "008", - "nickname": "Action H", + "nickname": "Behavior H", "type": "Dummy", }, - "Action I": { + # these will be the new behaviors that will be possible to "add" + # as part of new action4 , "add node" + # TODO: for now, show all actions, and user will just choose the new acton to add + + "Behavior I": { "id": "009", - "nickname": "Action I", + "nickname": "Behavior I", "type": "Dummy", }, - "Action J": { + "Behavior J": { "id": "010", - "nickname": "Action J", + "nickname": "Behavior J", "type": "Dummy", }, - "Action K": { + "Behavior K": { "id": "011", - "nickname": "Action K", + "nickname": "Behavior K", "type": "Dummy", }, } - def get_action_by_nickname(self, nickname): - return self.actions.get(nickname) + def get_behavior_by_nickname(self, nickname): + return self.behaviors.get(nickname) - def get_action_by_id(self, id): - for action in self.actions.values(): - if action.id == id: - return action + def get_behavior_by_id(self, id): + for behavior in self.behaviors.values(): + if behavior.id == id: + return behavior return None \ No newline at end of file diff --git a/src/social_norms_trees/custom_node_library.py b/src/social_norms_trees/custom_node_library.py index 40f7dd99..78ea6b29 100644 --- a/src/social_norms_trees/custom_node_library.py +++ b/src/social_norms_trees/custom_node_library.py @@ -1,7 +1,15 @@ import py_trees -class CustomAction(py_trees.behaviours.Dummy): +class CustomBehavior(py_trees.behaviours.Dummy): def __init__(self, name, id, nickname): super().__init__(name) - self.id = id - self.nickname = nickname \ No newline at end of file + self.id_ = id + self.nickname = nickname + + + + # id of the behavior within the behavior library (persists) + # but also the unique id for the behavior within the tree (in case there are multiple instances of + # the behavior in one tree) + + diff --git a/src/social_norms_trees/mutate_tree.py b/src/social_norms_trees/mutate_tree.py index 2129e1a7..7f652be2 100644 --- a/src/social_norms_trees/mutate_tree.py +++ b/src/social_norms_trees/mutate_tree.py @@ -347,7 +347,7 @@ def remove_node(tree: T, node: Optional[py_trees.behaviour.Behaviour] = None) -> "type": "remove_node", "nodes": [ { - "id": node.id, + "id_": node.id_, "nickname": node.nickname }, ], @@ -364,6 +364,7 @@ def move_node( node: Optional[py_trees.behaviour.Behaviour] = None, new_parent: Optional[py_trees.behaviour.Behaviour] = None, index: int = None, + internal_call: bool = False, ) -> T: """Exchange two behaviours in the tree @@ -384,23 +385,25 @@ def move_node( assert isinstance(new_parent, py_trees.composites.Composite) assert isinstance(node.parent, py_trees.composites.Composite) - old_parent = node.parent.name + # old_parent = node.parent.name node.parent.remove_child(node) new_parent.insert_child(node, index) - action_log = { - "type": "move_node", - "nodes": [ - { - "id": node.id, - "nickname": node.nickname, - "old parent": old_parent, - "new parent": node.parent.name - }, - ], - "timestamp": datetime.now().isoformat(), - } - return tree, action_log + + if not internal_call: + action_log = { + "type": "move_node", + "nodes": [ + { + "id": node.id_, + "nickname": node.nickname, + }, + ], + "timestamp": datetime.now().isoformat(), + } + return tree, action_log + + return tree def exchange_nodes( @@ -462,21 +465,41 @@ def exchange_nodes( node0_parent, node0_index = node0.parent, node0.parent.children.index(node0) node1_parent, node1_index = node1.parent, node1.parent.children.index(node1) - tree, _ = move_node(tree, node0, node1_parent, node1_index) - tree, _ = move_node(tree, node1, node0_parent, node0_index) + tree = move_node(tree, node0, node1_parent, node1_index, True) + tree = move_node(tree, node1, node0_parent, node0_index, True) - action_log = { - "type": "exchange_nodes", - "nodes": [ + nodes = [] + if node0.__class__.__name__ != "CustomBehavior": + nodes.append( + { + "nickname": node0.name, + } + ) + else: + nodes.append( { - "id": node0.id, + "id": node0.id_, "nickname": node0.nickname - }, + } + ) + + if node1.__class__.__name__ != "CustomBehavior": + nodes.append( { - "id": node1.id, + "nickname": node1.name, + } + ) + else: + nodes.append( + { + "id": node1.id_, "nickname": node1.nickname - }, - ], + } + ) + + action_log = { + "type": "exchange_nodes", + "nodes": nodes, "timestamp": datetime.now().isoformat(), } return tree, action_log \ No newline at end of file diff --git a/src/social_norms_trees/serialize_tree.py b/src/social_norms_trees/serialize_tree.py index ac6e1cbe..9928b645 100644 --- a/src/social_norms_trees/serialize_tree.py +++ b/src/social_norms_trees/serialize_tree.py @@ -1,5 +1,5 @@ import py_trees -from social_norms_trees.custom_node_library import CustomAction +from social_norms_trees.custom_node_library import CustomBehavior def serialize_tree(tree): @@ -13,22 +13,22 @@ def serialize_node(node): return serialize_node(tree) -def deserialize_tree(tree, action_library): +def deserialize_tree(tree, behavior_library): def deserialize_node(node): node_type = node['type'] children = [deserialize_node(child) for child in node['children']] if node_type == 'Sequence': return py_trees.composites.Sequence(node['name'], False, children=children) - elif node_type == 'Action': - action = action_library.get_action_by_nickname(node['name']) - if action: - return CustomAction( - name=action['nickname'], - id=action['id'], - nickname=action['nickname'] + elif node_type == 'Behavior': + behavior = behavior_library.get_behavior_by_nickname(node['name']) + if behavior: + return CustomBehavior( + name=behavior['nickname'], + id_=behavior['id'], + nickname=behavior['nickname'] ) else: - raise ValueError(f"Action {node['name']} not found in action library") + raise ValueError(f"Behavior {node['name']} not found in behavior library") return deserialize_node(tree) \ No newline at end of file diff --git a/src/social_norms_trees/ui_wrapper.py b/src/social_norms_trees/ui_wrapper.py index 0ee04d38..1410c0dd 100644 --- a/src/social_norms_trees/ui_wrapper.py +++ b/src/social_norms_trees/ui_wrapper.py @@ -9,7 +9,7 @@ from social_norms_trees.mutate_tree import move_node, exchange_nodes, remove_node from social_norms_trees.serialize_tree import serialize_tree, deserialize_tree -from social_norms_trees.action_library import ActionLibrary +from social_norms_trees.action_library import BehaviorLibrary DB_FILE = "db.json" @@ -29,13 +29,13 @@ def save_db(db, db_file): with open(db_file, "w") as f: json.dump(db, f, indent=4) -def experiment_setup(db, action_library): +def experiment_setup(db, behavior_library): print("\n") participant_id = participant_login() print("\n") - origin_tree = load_behavior_tree(action_library) + origin_tree = load_behavior_tree(behavior_library) experiment_id = initialize_experiment_record(db, participant_id, origin_tree) @@ -51,10 +51,6 @@ def participant_login(): return participant_id -# def get_behavior_trees(): -# #TODO: Get behavior trees from respective data structure - -# print("1. Original Tree") behavior_trees = { "tree_1": { @@ -62,13 +58,13 @@ def participant_login(): "type": "Sequence", "children": [ { - "name": "Action A", - "type": "Action", + "name": "Behavior A", + "type": "Behavior", "children": [] }, { - "name": "Action B", - "type": "Action", + "name": "Behavior B", + "type": "Behavior", "children": [] }, { @@ -76,13 +72,13 @@ def participant_login(): "type": "Sequence", "children": [ { - "name": "Action C", - "type": "Action", + "name": "Behavior C", + "type": "Behavior", "children": [] }, { - "name": "Action D", - "type": "Action", + "name": "Behavior D", + "type": "Behavior", "children": [] } ] @@ -92,23 +88,23 @@ def participant_login(): "type": "Sequence", "children": [ { - "name": "Action E", - "type": "Action", + "name": "Behavior E", + "type": "Behavior", "children": [] }, { - "name": "Action F", - "type": "Action", + "name": "Behavior F", + "type": "Behavior", "children": [] }, { - "name": "Action G", - "type": "Action", + "name": "Behavior G", + "type": "Behavior", "children": [] }, { - "name": "Action H", - "type": "Action", + "name": "Behavior H", + "type": "Behavior", "children": [] } ] @@ -118,7 +114,7 @@ def participant_login(): } -def load_behavior_tree(action_library): +def load_behavior_tree(behavior_library): for idx, tree_name in enumerate(behavior_trees.keys(), 1): print(f"{idx}. {tree_name}") @@ -126,7 +122,7 @@ def load_behavior_tree(action_library): tree_config = behavior_trees[f"tree_{selected_index}"] - experiment_ready_tree = deserialize_tree(tree_config, action_library) + experiment_ready_tree = deserialize_tree(tree_config, behavior_library) return experiment_ready_tree @@ -200,16 +196,21 @@ def run_experiment(db, participant_id, origin_tree, experiment_id): def main(): print("AIT Prototype #1 Simulator") - action_library = ActionLibrary() + behavior_library = BehaviorLibrary() + + #TODO: define a input file, that will have the original tree and also the behavior library + #TODO: write up some context, assumptions made in the README DB_FILE = "db.json" db = load_db(DB_FILE) - participant_id, origin_tree, experiment_id = experiment_setup(db, action_library) + participant_id, origin_tree, experiment_id = experiment_setup(db, behavior_library) db = run_experiment(db, participant_id, origin_tree, experiment_id) save_db(db, DB_FILE) + + #TODO: define export file, that will be where we export the results to print("\nSimulation has ended.") From d8a8290b70c658186628f6894c828b70153f1ce7 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 16 Sep 2024 10:53:19 -0400 Subject: [PATCH 7/7] move starting behavior_tree and behavior_tree into import file approach --- src/social_norms_trees/action_library.py | 73 ----------- src/social_norms_trees/behavior_library.py | 12 ++ src/social_norms_trees/custom_node_library.py | 4 +- src/social_norms_trees/mutate_tree.py | 62 ++++++++- src/social_norms_trees/resources.json | 104 +++++++++++++++ src/social_norms_trees/ui_wrapper.py | 121 ++++++------------ 6 files changed, 215 insertions(+), 161 deletions(-) delete mode 100644 src/social_norms_trees/action_library.py create mode 100644 src/social_norms_trees/behavior_library.py create mode 100644 src/social_norms_trees/resources.json diff --git a/src/social_norms_trees/action_library.py b/src/social_norms_trees/action_library.py deleted file mode 100644 index 3e1f29b6..00000000 --- a/src/social_norms_trees/action_library.py +++ /dev/null @@ -1,73 +0,0 @@ -class BehaviorLibrary: - def __init__(self): - self.behaviors = { - "Behavior A": { - "id": "001", - "nickname": "Behavior A", - "type": "Dummy", - }, - "Behavior B": { - "id": "002", - "nickname": "Behavior B", - "type": "Dummy", - }, - "Behavior C": { - "id": "003", - "nickname": "Behavior C", - "type": "Dummy", - }, - "Behavior D": { - "id": "004", - "nickname": "Behavior D", - "type": "Dummy", - }, - "Behavior E": { - "id": "005", - "nickname": "Behavior E", - "type": "Dummy", - }, - "Behavior F": { - "id": "006", - "nickname": "Behavior F", - "type": "Dummy", - }, - "Behavior G": { - "id": "007", - "nickname": "Behavior G", - "type": "Dummy", - }, - "Behavior H": { - "id": "008", - "nickname": "Behavior H", - "type": "Dummy", - }, - - # these will be the new behaviors that will be possible to "add" - # as part of new action4 , "add node" - # TODO: for now, show all actions, and user will just choose the new acton to add - - "Behavior I": { - "id": "009", - "nickname": "Behavior I", - "type": "Dummy", - }, - "Behavior J": { - "id": "010", - "nickname": "Behavior J", - "type": "Dummy", - }, - "Behavior K": { - "id": "011", - "nickname": "Behavior K", - "type": "Dummy", - }, - } - - def get_behavior_by_nickname(self, nickname): - return self.behaviors.get(nickname) - - def get_behavior_by_id(self, id): - for behavior in self.behaviors.values(): - if behavior.id == id: - return behavior - return None \ No newline at end of file diff --git a/src/social_norms_trees/behavior_library.py b/src/social_norms_trees/behavior_library.py new file mode 100644 index 00000000..9ba37d24 --- /dev/null +++ b/src/social_norms_trees/behavior_library.py @@ -0,0 +1,12 @@ +class BehaviorLibrary: + def __init__(self, behaviors): + self.behaviors = behaviors + + def get_behavior_by_nickname(self, nickname): + return self.behaviors.get(nickname) + + def get_behavior_by_id(self, id_): + for behavior in self.behaviors.values(): + if behavior.id == id_: + return behavior + return None \ No newline at end of file diff --git a/src/social_norms_trees/custom_node_library.py b/src/social_norms_trees/custom_node_library.py index 78ea6b29..9194cebd 100644 --- a/src/social_norms_trees/custom_node_library.py +++ b/src/social_norms_trees/custom_node_library.py @@ -1,9 +1,9 @@ import py_trees class CustomBehavior(py_trees.behaviours.Dummy): - def __init__(self, name, id, nickname): + def __init__(self, name, id_, nickname): super().__init__(name) - self.id_ = id + self.id_ = id_ self.nickname = nickname diff --git a/src/social_norms_trees/mutate_tree.py b/src/social_norms_trees/mutate_tree.py index 7f652be2..163daba5 100644 --- a/src/social_norms_trees/mutate_tree.py +++ b/src/social_norms_trees/mutate_tree.py @@ -10,6 +10,8 @@ from datetime import datetime +from social_norms_trees.custom_node_library import CustomBehavior + T = TypeVar("T", bound=py_trees.behaviour.Behaviour) @@ -502,4 +504,62 @@ def exchange_nodes( "nodes": nodes, "timestamp": datetime.now().isoformat(), } - return tree, action_log \ No newline at end of file + return tree, action_log + + +def prompt_select_node(behavior_library, text): + + for idx, tree_name in enumerate(behavior_library.behaviors.keys(), 1): + print(f"{idx}. {tree_name}") + + + node_index = click.prompt( + text=text, + type=int, + ) + + node_key = list(behavior_library.behaviors.keys())[node_index-1] + + return behavior_library.behaviors[node_key] + + +def add_node( + tree: T, + behavior_library: object, +) -> T: + """Exchange two behaviours in the tree + + Examples: + >>> tree = py_trees.composites.Sequence("", False, children=[]) + + """ + + + behavior = prompt_select_node(behavior_library, f"Which behavior do you want to add?") + + new_node = CustomBehavior( + name=behavior['nickname'], + id_=behavior['id'], + nickname=behavior['nickname'] + ) + + new_parent = prompt_identify_parent_node( + tree, f"What should its parent be?", display_nodes=True + ) + + index = prompt_identify_child_index(new_parent) + + assert isinstance(new_parent, py_trees.composites.Composite) + + new_parent.insert_child(new_node, index) + + action_log = { + "type": "add_node", + "node": { + "id": new_node.id_, + "nickname": new_node.nickname + }, + "timestamp": datetime.now().isoformat(), + } + + return tree, action_log diff --git a/src/social_norms_trees/resources.json b/src/social_norms_trees/resources.json new file mode 100644 index 00000000..78363361 --- /dev/null +++ b/src/social_norms_trees/resources.json @@ -0,0 +1,104 @@ +{ + "behavior_tree": { + "name": "Sequence A", + "type": "Sequence", + "children": [ + { + "name": "Behavior A", + "type": "Behavior", + "children": [] + }, + { + "name": "Behavior B", + "type": "Behavior", + "children": [] + }, + { + "name": "Sequence B", + "type": "Sequence", + "children": [ + { + "name": "Behavior C", + "type": "Behavior", + "children": [] + }, + { + "name": "Behavior D", + "type": "Behavior", + "children": [] + } + ] + }, + { + "name": "Sequence C", + "type": "Sequence", + "children": [ + { + "name": "Behavior E", + "type": "Behavior", + "children": [] + }, + { + "name": "Behavior F", + "type": "Behavior", + "children": [] + }, + { + "name": "Behavior G", + "type": "Behavior", + "children": [] + }, + { + "name": "Behavior H", + "type": "Behavior", + "children": [] + } + ] + } + ] + }, + "behavior_library": + { + "Behavior A": { + "id": "001", + "nickname": "Behavior A", + "type": "Dummy" + }, + "Behavior B": { + "id": "002", + "nickname": "Behavior B", + "type": "Dummy" + }, + "Behavior C": { + "id": "003", + "nickname": "Behavior C", + "type": "Dummy" + }, + "Behavior D": { + "id": "004", + "nickname": "Behavior D", + "type": "Dummy" + }, + "Behavior E": { + "id": "005", + "nickname": "Behavior E", + "type": "Dummy" + }, + "Behavior F": { + "id": "006", + "nickname": "Behavior F", + "type": "Dummy" + }, + "Behavior G": { + "id": "007", + "nickname": "Behavior G", + "type": "Dummy" + }, + "Behavior H": { + "id": "008", + "nickname": "Behavior H", + "type": "Dummy" + } + + } +} diff --git a/src/social_norms_trees/ui_wrapper.py b/src/social_norms_trees/ui_wrapper.py index 1410c0dd..ebd745d4 100644 --- a/src/social_norms_trees/ui_wrapper.py +++ b/src/social_norms_trees/ui_wrapper.py @@ -6,10 +6,10 @@ import uuid import py_trees -from social_norms_trees.mutate_tree import move_node, exchange_nodes, remove_node +from social_norms_trees.mutate_tree import move_node, exchange_nodes, remove_node, add_node from social_norms_trees.serialize_tree import serialize_tree, deserialize_tree -from social_norms_trees.action_library import BehaviorLibrary +from social_norms_trees.behavior_library import BehaviorLibrary DB_FILE = "db.json" @@ -29,19 +29,16 @@ def save_db(db, db_file): with open(db_file, "w") as f: json.dump(db, f, indent=4) -def experiment_setup(db, behavior_library): +def experiment_setup(db, origin_tree): print("\n") participant_id = participant_login() - print("\n") - origin_tree = load_behavior_tree(behavior_library) - experiment_id = initialize_experiment_record(db, participant_id, origin_tree) print("\nSetup Complete.\n") - return participant_id, origin_tree, experiment_id + return participant_id, experiment_id def participant_login(): @@ -51,81 +48,27 @@ def participant_login(): return participant_id +def load_resources(file_path): + try: + print(f"\nLoading behavior tree and behavior library from {file_path}...\n") + with open(file_path, 'r') as file: + resources = json.load(file) + + except json.JSONDecodeError: + raise ValueError("Error") + except Exception: + raise RuntimeError("Error") -behavior_trees = { - "tree_1": { - "name": "Sequence A", - "type": "Sequence", - "children": [ - { - "name": "Behavior A", - "type": "Behavior", - "children": [] - }, - { - "name": "Behavior B", - "type": "Behavior", - "children": [] - }, - { - "name": "Sequence B", - "type": "Sequence", - "children": [ - { - "name": "Behavior C", - "type": "Behavior", - "children": [] - }, - { - "name": "Behavior D", - "type": "Behavior", - "children": [] - } - ] - }, - { - "name": "Sequence C", - "type": "Sequence", - "children": [ - { - "name": "Behavior E", - "type": "Behavior", - "children": [] - }, - { - "name": "Behavior F", - "type": "Behavior", - "children": [] - }, - { - "name": "Behavior G", - "type": "Behavior", - "children": [] - }, - { - "name": "Behavior H", - "type": "Behavior", - "children": [] - } - ] - } - ] - }, -} - - -def load_behavior_tree(behavior_library): - for idx, tree_name in enumerate(behavior_trees.keys(), 1): - print(f"{idx}. {tree_name}") - - selected_index = click.prompt("Please select a behavior tree to load for the experiment (enter the number)", type=int) - - tree_config = behavior_trees[f"tree_{selected_index}"] - - experiment_ready_tree = deserialize_tree(tree_config, behavior_library) - - return experiment_ready_tree + behavior_tree = resources.get('behavior_tree') + behaviors = resources.get('behavior_library') + + behavior_library = BehaviorLibrary(behaviors) + + behavior_tree = deserialize_tree(behavior_tree, behavior_library) + + print("Loading success.") + return behavior_tree, behavior_library def initialize_experiment_record(db, participant_id, origin_tree): @@ -146,7 +89,7 @@ def initialize_experiment_record(db, participant_id, origin_tree): return experiment_id -def run_experiment(db, participant_id, origin_tree, experiment_id): +def run_experiment(db, origin_tree, experiment_id, behavior_library): # Loop for the actual experiment part, which takes user input to decide which action to take print("\nExperiment beginning...\n") @@ -165,8 +108,9 @@ def run_experiment(db, participant_id, origin_tree, experiment_id): "1. move node\n" + "2. exchange node\n" + "3. remove node\n" + + "4. add node\n" + "Please select an action to perform on the behavior tree", - type=click.Choice(['1', '2', '3'], case_sensitive=False), + type=click.Choice(['1', '2', '3', '4'], case_sensitive=False), show_choices=True ) @@ -181,6 +125,10 @@ def run_experiment(db, participant_id, origin_tree, experiment_id): origin_tree, action_log = remove_node(origin_tree) db[experiment_id]["action_history"].append(action_log) + elif action == "4": + origin_tree, action_log = add_node(origin_tree, behavior_library) + db[experiment_id]["action_history"].append(action_log) + else: print("Invalid choice, please select a valid number (1, 2, or 3).\n") @@ -196,7 +144,6 @@ def run_experiment(db, participant_id, origin_tree, experiment_id): def main(): print("AIT Prototype #1 Simulator") - behavior_library = BehaviorLibrary() #TODO: define a input file, that will have the original tree and also the behavior library #TODO: write up some context, assumptions made in the README @@ -204,9 +151,13 @@ def main(): DB_FILE = "db.json" db = load_db(DB_FILE) + #load tree to run experiment on, and behavior library + + RESOURCES_FILE = "resources.json" + original_tree, behavior_library = load_resources(RESOURCES_FILE) - participant_id, origin_tree, experiment_id = experiment_setup(db, behavior_library) - db = run_experiment(db, participant_id, origin_tree, experiment_id) + participant_id, experiment_id = experiment_setup(db, original_tree) + db = run_experiment(db, original_tree, experiment_id, behavior_library) save_db(db, DB_FILE)