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 new file mode 100644 index 00000000..9194cebd --- /dev/null +++ b/src/social_norms_trees/custom_node_library.py @@ -0,0 +1,15 @@ +import py_trees + +class CustomBehavior(py_trees.behaviours.Dummy): + def __init__(self, name, id_, nickname): + super().__init__(name) + 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 dd12024d..163daba5 100644 --- a/src/social_norms_trees/mutate_tree.py +++ b/src/social_norms_trees/mutate_tree.py @@ -8,6 +8,10 @@ import click import py_trees +from datetime import datetime + +from social_norms_trees.custom_node_library import CustomBehavior + T = TypeVar("T", bound=py_trees.behaviour.Behaviour) @@ -337,12 +341,24 @@ 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 = {} + return tree, elif isinstance(parent_node, py_trees.composites.Composite): parent_node.remove_child(node) + action_log = { + "type": "remove_node", + "nodes": [ + { + "id_": node.id_, + "nickname": node.nickname + }, + ], + "timestamp": datetime.now().isoformat(), + } else: raise NotImplementedError() - return tree + + return tree, action_log def move_node( @@ -350,6 +366,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 @@ -370,9 +387,24 @@ 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) + + 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 @@ -435,7 +467,99 @@ 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) + + nodes = [] + if node0.__class__.__name__ != "CustomBehavior": + nodes.append( + { + "nickname": node0.name, + } + ) + else: + nodes.append( + { + "id": node0.id_, + "nickname": node0.nickname + } + ) + + if node1.__class__.__name__ != "CustomBehavior": + nodes.append( + { + "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 + + +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) - return tree \ No newline at end of file + 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/serialize_tree.py b/src/social_norms_trees/serialize_tree.py index 2db28d69..9928b645 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 CustomBehavior def serialize_tree(tree): @@ -12,20 +13,22 @@ def serialize_node(node): return serialize_node(tree) -def deserialize_tree(tree): +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 == '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 == '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"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 4a56b3d5..ebd745d4 100644 --- a/src/social_norms_trees/ui_wrapper.py +++ b/src/social_norms_trees/ui_wrapper.py @@ -6,9 +6,11 @@ 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.behavior_library import BehaviorLibrary + DB_FILE = "db.json" @@ -27,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): +def experiment_setup(db, origin_tree): print("\n") participant_id = participant_login() - print("\n") - origin_tree = load_behavior_tree() - 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(): @@ -49,46 +48,27 @@ 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 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_tree = resources.get('behavior_tree') + behaviors = resources.get('behavior_library') -def load_behavior_tree(): - - tree_array = get_behavior_trees() - 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] + 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): @@ -96,14 +76,12 @@ 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, "base_behavior_tree": serialize_tree(origin_tree), "start_date": datetime.now().isoformat(), - "actions": [], + "action_history": [], } db[experiment_id] = experiment_record @@ -111,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") @@ -130,20 +108,27 @@ 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 ) 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) + + 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") @@ -159,14 +144,24 @@ def run_experiment(db, participant_id, origin_tree, experiment_id): def main(): print("AIT Prototype #1 Simulator") + + #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) + #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) - 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) + + #TODO: define export file, that will be where we export the results to print("\nSimulation has ended.")