From 29a8e147f32ed1a962d13096bf324da0546e75ee Mon Sep 17 00:00:00 2001 From: Ibrahim Alhas <65875290+alhasacademy96@users.noreply.github.com> Date: Mon, 4 Sep 2023 21:09:38 +0100 Subject: [PATCH 01/10] Updated raycaster.py Added code to dynamically adjust the size of the raycast array + added better testing for a variety of settings + added improved method descriptions for clarity. --- animalai/animalai/envs/raycastparser.py | 165 +++++++++++++----------- 1 file changed, 88 insertions(+), 77 deletions(-) diff --git a/animalai/animalai/envs/raycastparser.py b/animalai/animalai/envs/raycastparser.py index d138801eb..ee6f736f8 100644 --- a/animalai/animalai/envs/raycastparser.py +++ b/animalai/animalai/envs/raycastparser.py @@ -1,10 +1,8 @@ import enum import numpy as np -"""Parses the raycast observations from AnimalAI and returns a shortened version with only relevant objects""" class RayCastObjects(enum.Enum): - """Enum for the parsed objects from the raycast. The order must match the order in the Unity environment.""" ARENA = 0 IMMOVABLE = 1 MOVABLE = 2 @@ -12,63 +10,74 @@ class RayCastObjects(enum.Enum): GOODGOALMULTI = 4 BADGOAL = 5 GOALSPAWNER = 6 - INNERWALL = 7 - OUTERWALL = 8 - DEATHZONE = 9 - HOTZONE = 10 - RAMP = 11 - PILLAR_BUTTON = 12 + DEATHZONE = 7 + HOTZONE = 8 + RAMP = 9 + PILLARBUTTON = 10 class RayCastParser(): - numberDetectableObjects = 13 # This is defined in the Unity environment - """Parses the raycast observations from AnimalAI and returns a shortened version with only relevant objects - replaces the one-hot vector with the distance to the object (if any were hit) - listOfObjects is an array of all the objects that you care about (as RayCAstObjects enum) - also reorders the array so that it is read from left to right""" + """ + The RayCastParser class is responsible for parsing raycast observations + from the AnimalAI environment and returning a simplified version that only + contains relevant objects. + """ def __init__(self, listOfObjects, numberOfRays): - """Initialize the parser""" + """ + Initialize the RayCastParser. + + Parameters: + - listOfObjects: List of objects (from RayCastObjects enum) that the parser should look for. + - numberOfRays: The number of rays in the raycast from AnimalAI environment. + """ self.numberOfRays = numberOfRays self.listOfObjects = listOfObjects self.listofObjectVals = [x.value for x in listOfObjects] self.numberOfObjects = len(listOfObjects) def parse(self, raycast) -> np.ndarray: - """Parse the raycast - input: the raycast direct from Unity - output: a shortened version with only the object in listOfObjects - output is an array with one row for every element of listOfObjects - reordered to read fro left to right""" + """ + Parse the raw raycast data and return a simplified array. - print(f"Initial raycast: {raycast}") - print(f"List of Objects: {self.listOfObjects}") + Parameters: + - raycast: The raw raycast array from the AnimalAI environment. - assert (len(raycast) == self.numberOfRays * - (self.numberDetectableObjects+2)) + Returns: + - np.ndarray: The parsed raycast, simplified to only include objects in listOfObjects. + """ + self.numberDetectableObjects = int( + len(raycast) / self.numberOfRays) - 2 parsedRaycast = np.zeros((len(self.listOfObjects), self.numberOfRays)) - print(parsedRaycast) for i in range(self.numberOfRays): for j in range(self.numberDetectableObjects): - if j in self.listofObjectVals: - if raycast[i * (self.numberDetectableObjects + 2) + j] == 1: - parsedRaycast[self.listofObjectVals.index(j)][i] = ( - raycast[i * (self.numberDetectableObjects + 2) + self.numberDetectableObjects + 1]) - print( - f"Detected object {j} at ray {i} with distance {raycast[i * (self.numberDetectableObjects + 2) + self.numberDetectableObjects + 1]}") - # Change flattened array into matrix with one row per object in listOfObjects + idx = i * (self.numberDetectableObjects + 2) + j + distance_idx = i * \ + (self.numberDetectableObjects + 2) + \ + self.numberDetectableObjects + 1 + if idx < len(raycast) and j in self.listofObjectVals: + if raycast[idx] == 1 and distance_idx < len(raycast): + parsedRaycast[self.listofObjectVals.index( + j)][i] = raycast[distance_idx] + parsedRaycast = np.reshape( parsedRaycast, (len(self.listOfObjects), self.numberOfRays)) reordered = np.zeros_like(parsedRaycast) for i in range(parsedRaycast.shape[0]): reordered[i] = self.reorderRow(parsedRaycast[i]) - - print(f"parsedRaycast after parsing: {parsedRaycast}") - print(f"Reordered Raycast: {reordered}") return reordered def reorderRow(self, row): - """Reorders the row so instead of labelling from middle, lables from left to right""" + """ + Reorder the elements in a row so that they read from left to right, + as opposed to from the middle outwards. + + Parameters: + - row: A 1D numpy array representing a single row of parsed raycast data. + + Returns: + - np.ndarray: The reordered row. + """ newRow = np.zeros_like(row) midIndex = int((self.numberOfRays-1)/2) newRow[midIndex] = row[0] @@ -78,57 +87,59 @@ def reorderRow(self, row): return newRow def prettyPrint(self, raycast) -> str: - """Parses the raycast and outputs a human readable version""" + """ + Pretty-prints the parsed raycast data, making it easier to read and understand. + + Parameters: + - raycast: The raw raycast array from the AnimalAI environment. + + Prints the parsed and simplified raycast in a human-readable format. + """ parsedRaycast = self.parse(raycast) for i in range(parsedRaycast.shape[0]): print(self.listOfObjects[i].name, ":", parsedRaycast[i]) if __name__ == "__main__": - """Test the parsing works - Only a few sanity checks""" + # Test 1: Simple test to check if GOODGOAL and IMMOVABLE objects are correctly parsed + # Description: This test checks if the parser correctly identifies GOODGOAL and IMMOVABLE objects + # and places them correctly in the parsed raycast array. rayParser = RayCastParser( [RayCastObjects.GOODGOAL, RayCastObjects.IMMOVABLE], 5) - parsedRaycast = rayParser.parse([1, 1, 1, 1, 1, 1, 0, 0.1, - 1, 1, 1, 1, 1, 1, 0, 0.2, - 1, 1, 1, 1, 1, 1, 1, 0.3, - 1, 1, 1, 1, 1, 1, 1, 0.4, - 1, 1, 1, 1, 1, 1, 1, 0.5]) - assert (np.array_equal(parsedRaycast, np.array( - [[0.4, 0.2, 0.1, 0.3, 0.5], [0.4, 0.2, 0.1, 0.3, 0.5]]))) - rayParser = RayCastParser( - [RayCastObjects.GOODGOAL, RayCastObjects.IMMOVABLE, RayCastObjects.BADGOAL], 3) - parsedRaycast = rayParser.parse([0, 0, 0, 0, 0, 0, 0, 0.1, - 0, 0, 0, 0, 0, 0, 0, 0.2, - 0, 0, 0, 0, 0, 0, 1, 0.3]) - assert (np.array_equal(parsedRaycast, np.array( - [[0, 0, 0], [0, 0, 0], [0, 0, 0]]))) - rayParser = RayCastParser( - [RayCastObjects.ARENA, RayCastObjects.MOVABLE, RayCastObjects.GOODGOALMULTI], 7) - parsedRaycast = rayParser.parse([1, 0, 0, 0, 0, 0, 0, 0.1, - 0, 1, 0, 0, 0, 0, 0, 0.2, - 0, 0, 1, 0, 0, 0, 1, 0.3, - 0, 0, 0, 1, 0, 0, 1, 0.4, - 0, 0, 0, 0, 1, 0, 1, 0.5, - 0, 0, 0, 0, 0, 1, 1, 0.6, - 0, 0, 0, 0, 0, 0, 0, 0]) - assert (np.array_equal(parsedRaycast, np.array( - [[0, 0, 0, 0.1, 0, 0, 0], [0, 0, 0, 0, 0.3, 0, 0], [0, 0, 0, 0, 0, 0.5, 0]]))) + test_raycast = [1, 1, 1, 1, 1, 1, 0, 0.1, + 1, 1, 1, 1, 1, 1, 0, 0.2, + 1, 1, 1, 1, 1, 1, 1, 0.3, + 1, 1, 1, 1, 1, 1, 1, 0.4, + 1, 1, 1, 1, 1, 1, 1, 0.5] + parsedRaycast = rayParser.parse(test_raycast) + print("Parsed Raycast for Test 1:") + print(parsedRaycast) + rayParser.prettyPrint(test_raycast) + + # Test 2: Checking for no objects detected + # Description: This test checks if the parser correctly identifies when no objects are detected. rayParser = RayCastParser( - [RayCastObjects.GOODGOAL, RayCastObjects.IMMOVABLE, RayCastObjects.GOALSPAWNER], 5) - parsedRaycast = rayParser.parse([1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0.1, - 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0.2, - 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0.3, - 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0.4, - 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0.5]) + [RayCastObjects.GOODGOAL, RayCastObjects.BADGOAL], 3) + test_raycast = [0, 0, 0, 0, 0, 0, 0, 0.1, + 0, 0, 0, 0, 0, 0, 0, 0.2, + 0, 0, 0, 0, 0, 0, 0, 0.3] + parsedRaycast = rayParser.parse(test_raycast) + print("Parsed Raycast for Test 2:") print(parsedRaycast) - assert (np.array_equal(parsedRaycast, np.array([[0.4, 0.2, 0.1, 0.3, 0.5], [ - 0.4, 0.2, 0.1, 0.3, 0.5], [0.4, 0.2, 0.1, 0.3, 0.5]]))) + rayParser.prettyPrint(test_raycast) + + # Test 3: Mix of objects detected and not detected + # Description: This test checks if the parser correctly identifies some objects while ignoring others. rayParser = RayCastParser( - [RayCastObjects.GOODGOAL, RayCastObjects.IMMOVABLE, RayCastObjects.PILLAR_BUTTON], 5) - parsedRaycast = rayParser.parse([1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0.1, - 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0.2, - 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0.3, - 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0.4, - 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0.5]) + [RayCastObjects.ARENA, RayCastObjects.MOVABLE, RayCastObjects.GOODGOALMULTI], 7) + test_raycast = [1, 0, 0, 0, 0, 0, 0, 0.1, + 0, 1, 0, 0, 0, 0, 0, 0.2, + 0, 0, 1, 0, 0, 0, 1, 0.3, + 0, 0, 0, 1, 0, 0, 1, 0.4, + 0, 0, 0, 0, 1, 0, 1, 0.5, + 0, 0, 0, 0, 0, 1, 1, 0.6, + 0, 0, 0, 0, 0, 0, 0, 0] + parsedRaycast = rayParser.parse(test_raycast) + print("Parsed Raycast for Test 3:") print(parsedRaycast) + rayParser.prettyPrint(test_raycast) From 736be7ea37c86ca96b45c9ffd75b47820f7ab2c8 Mon Sep 17 00:00:00 2001 From: Ibrahim Alhas <65875290+alhasacademy96@users.noreply.github.com> Date: Mon, 4 Sep 2023 21:11:47 +0100 Subject: [PATCH 02/10] Update raycastparser.py added missing descriptions to overall script and RaycastObjects() method. --- animalai/animalai/envs/raycastparser.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/animalai/animalai/envs/raycastparser.py b/animalai/animalai/envs/raycastparser.py index ee6f736f8..6cc744908 100644 --- a/animalai/animalai/envs/raycastparser.py +++ b/animalai/animalai/envs/raycastparser.py @@ -1,8 +1,18 @@ import enum import numpy as np +""" +The script is designed to parse raycast observations from the AnimalAI environment. +It includes a class to interpret these observations and output a simplified version +of the data that only contains relevant objects. +""" + class RayCastObjects(enum.Enum): + """ + Enumeration of possible objects detected by the raycast. + The values should correspond with how they are defined in the AnimalAI Unity environment. + """ ARENA = 0 IMMOVABLE = 1 MOVABLE = 2 From cf5f89d2f8ed031742d9c4c559e0db5a00795ae7 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 7 Sep 2023 16:40:16 -0700 Subject: [PATCH 03/10] Update egg with some extra dependencies --- animalai/animalai.egg-info/PKG-INFO | 7 ++++--- animalai/animalai.egg-info/SOURCES.txt | 13 ++++++++++++- animalai/animalai.egg-info/requires.txt | 2 ++ animalai/animalai.egg-info/top_level.txt | 2 +- animalai/setup.py | 9 +++++---- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/animalai/animalai.egg-info/PKG-INFO b/animalai/animalai.egg-info/PKG-INFO index 2f3704bb0..c783b4073 100644 --- a/animalai/animalai.egg-info/PKG-INFO +++ b/animalai/animalai.egg-info/PKG-INFO @@ -2,13 +2,14 @@ Metadata-Version: 2.1 Name: animalai Version: 3.0.1 Summary: Animal AI environment Python API -Home-page: https://github.com/mdcrosby/animal-ai -Author: Matt Crosby -Author-email: matt@mdcrosby.com +Home-page: https://github.com/Kinds-of-Intelligence-CFI/animal-ai +Author: Matt Crosby; Ibrahim Alhas; K. Voudouris; W. Schellaert +Author-email: ia424@cam.ac.uk Classifier: Intended Audience :: Developers Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 Requires-Python: >=3.6 diff --git a/animalai/animalai.egg-info/SOURCES.txt b/animalai/animalai.egg-info/SOURCES.txt index ee63c1fc2..9b3dd911c 100644 --- a/animalai/animalai.egg-info/SOURCES.txt +++ b/animalai/animalai.egg-info/SOURCES.txt @@ -1,7 +1,18 @@ setup.py +animalai/__init__.py animalai.egg-info/PKG-INFO animalai.egg-info/SOURCES.txt animalai.egg-info/dependency_links.txt animalai.egg-info/not-zip-safe animalai.egg-info/requires.txt -animalai.egg-info/top_level.txt \ No newline at end of file +animalai.egg-info/top_level.txt +animalai/envs/__init__.py +animalai/envs/actions.py +animalai/envs/braitenberg.py +animalai/envs/environment.py +animalai/envs/raycastparser.py +animalai/envs/settings.py +animalai/envs/gym/__init__.py +animalai/envs/gym/environment.py +animalai/train/__init__.py +animalai/train/train.py \ No newline at end of file diff --git a/animalai/animalai.egg-info/requires.txt b/animalai/animalai.egg-info/requires.txt index ced01fa56..39f5fbc5d 100644 --- a/animalai/animalai.egg-info/requires.txt +++ b/animalai/animalai.egg-info/requires.txt @@ -1 +1,3 @@ mlagents==0.30.0 +numpy==1.21.2 +scipy==1.7.2 diff --git a/animalai/animalai.egg-info/top_level.txt b/animalai/animalai.egg-info/top_level.txt index 8b1378917..1b03e7323 100644 --- a/animalai/animalai.egg-info/top_level.txt +++ b/animalai/animalai.egg-info/top_level.txt @@ -1 +1 @@ - +animalai diff --git a/animalai/setup.py b/animalai/setup.py index 77d245efb..dde5e478a 100644 --- a/animalai/setup.py +++ b/animalai/setup.py @@ -2,11 +2,11 @@ setup( name="animalai", - version="3.0.1", + version="3.1.1", description="Animal AI environment Python API", - url="https://github.com/mdcrosby/animal-ai", - author="Matt Crosby", - author_email="matt@mdcrosby.com", + url="https://github.com/Kinds-of-Intelligence-CFI/animal-ai", + author="Matt Crosby; Ibrahim Alhas; K. Voudouris; W. Schellaert", + author_email="ia424@cam.ac.uk", classifiers=[ "Intended Audience :: Developers", "Topic :: Scientific/Engineering :: Artificial Intelligence", @@ -14,6 +14,7 @@ "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", ], packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), zip_safe=False, From 9ca377dc737cea0f65d24a8fb05e5c626945ce3e Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 7 Sep 2023 16:40:58 -0700 Subject: [PATCH 04/10] Update raycast parser with some minor utility changes --- animalai/animalai/envs/raycastparser.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/animalai/animalai/envs/raycastparser.py b/animalai/animalai/envs/raycastparser.py index 6cc744908..91320f61a 100644 --- a/animalai/animalai/envs/raycastparser.py +++ b/animalai/animalai/envs/raycastparser.py @@ -56,6 +56,9 @@ def parse(self, raycast) -> np.ndarray: Returns: - np.ndarray: The parsed raycast, simplified to only include objects in listOfObjects. """ + if isinstance(raycast, dict): + raycast = raycast['rays'] + self.numberDetectableObjects = int( len(raycast) / self.numberOfRays) - 2 parsedRaycast = np.zeros((len(self.listOfObjects), self.numberOfRays)) @@ -105,6 +108,10 @@ def prettyPrint(self, raycast) -> str: Prints the parsed and simplified raycast in a human-readable format. """ + + if isinstance(raycast, dict): + raycast = raycast['rays'] + parsedRaycast = self.parse(raycast) for i in range(parsedRaycast.shape[0]): print(self.listOfObjects[i].name, ":", parsedRaycast[i]) @@ -152,4 +159,4 @@ def prettyPrint(self, raycast) -> str: parsedRaycast = rayParser.parse(test_raycast) print("Parsed Raycast for Test 3:") print(parsedRaycast) - rayParser.prettyPrint(test_raycast) + rayParser.prettyPrint(test_raycast) \ No newline at end of file From 7f2643a2b1c79ca67f20dcdadaf8cd0b0a877a0e Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 7 Sep 2023 16:42:34 -0700 Subject: [PATCH 05/10] Add working braitenberg vehicle --- agents/basicBraitenberg.py | 250 ++++++++++++++++++++++++++++++++++ agents/goToGoodBraitenberg.py | 71 ---------- 2 files changed, 250 insertions(+), 71 deletions(-) create mode 100644 agents/basicBraitenberg.py delete mode 100644 agents/goToGoodBraitenberg.py diff --git a/agents/basicBraitenberg.py b/agents/basicBraitenberg.py new file mode 100644 index 000000000..16ac06dd1 --- /dev/null +++ b/agents/basicBraitenberg.py @@ -0,0 +1,250 @@ +import argparse +import os +import random +import numpy as np + +from animalai.envs.actions import AAIActions, AAIAction +from animalai.envs.environment import AnimalAIEnvironment +from animalai.envs.raycastparser import RayCastParser +from animalai.envs.raycastparser import RayCastObjects + +class Braitenberg(): + """Implements a simple heuristic agent (Braitenberg Vehicle) + It heads towards good goals and away from bad goals. + It navigates around immoveable objects directly ahead + If it is stationary it turns around + """ + def __init__(self, no_rays, max_degrees, verbose=False): + self.verbose = verbose # do you want to see the observations and actions? + self.no_rays = no_rays # how many rays should the agent have? + assert(self.no_rays % 2 == 1), "Number of rays must be an odd number." + self.max_degrees = max_degrees # how many degrees do you want the rays spread over? + """ + We specify six types of objects here. This set can be expanded to include more objects if you wish to design further rules. + """ + self.listOfObjects = [RayCastObjects.ARENA, + RayCastObjects.IMMOVABLE, + RayCastObjects.MOVABLE, + RayCastObjects.GOODGOAL, + RayCastObjects.GOODGOALMULTI, + RayCastObjects.BADGOAL] + + self.raycast_parser = RayCastParser(self.listOfObjects, self.no_rays) #initialize a class to parse raycasts for these objects + self.actions = AAIActions() # initalise the action set + self.prev_action = self.actions.NOOP # initialise the first action, chosen to be forwards + + def prettyPrint(self, obs) -> str: + """Prints the parsed observation""" + return self.raycast_parser.prettyPrint(obs) #prettyprints the observation in a nice format + + def checkStationarity(self, raycast): #checks whether the agent is stationary by examining its velocities. If they are below 1 in all directions, it turns. + vel_observations = raycast['velocity'] + if self.verbose: + print("Velocity observations") + print(vel_observations) + bool_array = (vel_observations <= np.array([1,1,1])) + if sum(bool_array) == 3: + return True + else: + return False + + def get_action(self, observations) -> AAIAction: #select an action based on observations + """Returns the action to take given the current parsed raycast observation and other observations""" + obs = self.raycast_parser.parse(observations) + if self.verbose: + print("Raw Raycast Observations:") + print(obs) + print("Pretty Raycast Observations:") + self.raycast_parser.prettyPrint(observations) + + newAction = self.actions.FORWARDS.action_tuple #initialise the new action to be no action + + """ + If the agent is stationary, and it hasn't previously gone forwardsleft or forwardsright, it must be the first step. So choose one of those actions at random (p(0.5)) + """ + if self.checkStationarity(observations) and self.prev_action != self.actions.FORWARDSLEFT and self.prev_action != self.actions.FORWARDSRIGHT: + select_LR = random.randint(0,1) + if select_LR == 0: + newAction = self.actions.FORWARDSLEFT + else: + newAction = self.actions.FORWARDSRIGHT + elif self.checkStationarity(observations) and self.prev_action == self.actions.FORWARDSLEFT: # otherwise if stationary, continue in the same direction (it must be stuck by an obstacle) + newAction = self.actions.FORWARDSLEFT + elif self.checkStationarity(observations) and self.prev_action == self.actions.FORWARDSRIGHT: + newAction = self.actions.FORWARDSRIGHT + elif self.ahead(obs, RayCastObjects.GOODGOALMULTI) and not self.checkStationarity(observations): #if it's not stationary and it sees a good goal ahead, go forwards + newAction = self.actions.FORWARDS + elif self.left(obs, RayCastObjects.GOODGOALMULTI) and not self.checkStationarity(observations): # if it's to the left, rotate left + newAction = self.actions.LEFT + elif self.right(obs, RayCastObjects.GOODGOALMULTI) and not self.checkStationarity(observations): # if it's to the right, rotate right + newAction = self.actions.RIGHT + elif self.ahead(obs, RayCastObjects.GOODGOAL) and not self.checkStationarity(observations): #as above for good goals + newAction = self.actions.FORWARDS + elif self.left(obs, RayCastObjects.GOODGOAL) and not self.checkStationarity(observations): + newAction = self.actions.LEFT + elif self.right(obs, RayCastObjects.GOODGOAL) and not self.checkStationarity(observations): + newAction = self.actions.RIGHT + elif self.ahead(obs, RayCastObjects.BADGOAL) and not self.checkStationarity(observations): #the opposite for bad goals + newAction = self.actions.BACKWARDS + elif self.left(obs, RayCastObjects.BADGOAL) and not self.checkStationarity(observations): + newAction = self.actions.RIGHT + elif self.right(obs, RayCastObjects.BADGOAL) and not self.checkStationarity(observations): + newAction = self.actions.LEFT + elif self.ahead(obs, RayCastObjects.IMMOVABLE) and not self.checkStationarity(observations): # if there is an obstacle ahead move forwardsleft or forwardsright randomly to start navigating around it + select_LR = random.randint(0,1) + if select_LR == 0: + newAction = self.actions.FORWARDSLEFT + else: + newAction = self.actions.FORWARDSRIGHT + # Otherwise, if there is an obstacle ahead and the previous action was forwardsleft OR if there is an obstacle to the left and nothing ahead and the agent is not stationary, continue forwardsleft to continue navigating around + elif ((self.ahead(obs, RayCastObjects.IMMOVABLE) and self.prev_action == self.actions.FORWARDSLEFT) or (self.left(obs, RayCastObjects.IMMOVABLE) and not self.ahead(obs, RayCastObjects.IMMOVABLE))) and not self.checkStationarity(observations): + newAction = self.actions.FORWARDSLEFT + # vice versa for if the right side. This way the agent can navigate around obstacles + elif ((self.ahead(obs, RayCastObjects.IMMOVABLE) and self.prev_action == self.actions.FORWARDSRIGHT) or (self.right(obs, RayCastObjects.IMMOVABLE) and not self.ahead(obs, RayCastObjects.IMMOVABLE))) and not self.checkStationarity(observations): + newAction = self.actions.FORWARDSRIGHT + else: #otherwise, continue the same action as before + newAction = self.prev_action + self.prev_action = newAction + + if self.verbose: + print("Action selected:") + print(newAction.name) + newActionTuple = newAction.action_tuple + + return newActionTuple + + def ahead(self, obs, object): #returns a true if the object is detected in the middle ray. + """Returns true if the input object is ahead of the agent""" + if(obs[self.listOfObjects.index(object)][int((self.no_rays-1)/2)] > 0): + if self.verbose: + print("found " + str(object) + " ahead") + return True + return False + + def left(self, obs, object): #returns a true if the object is in one of the left rays + """Returns true if the input object is left of the agent""" + for i in range(int((self.no_rays-1)/2)): + if(obs[self.listOfObjects.index(object)][i] > 0): + if self.verbose: + print("found " + str(object) + " left") + return True + return False + + def right(self, obs, object): #returns a true if the object is in one of the right rays + """Returns true if the input object is right of the agent""" + for i in range(int((self.no_rays-1)/2)): + if(obs[self.listOfObjects.index(object)][i+int((self.no_rays-1)/2) + 1] > 0): + if self.verbose: + print("found " + str(object) + " right") + return True + return False + +""" +A helper function to watch the agent on a single config. +You must run this config from the agents directory. +""" + +def watch_braitenberg_agent_single_config(configuration_file: str, agent: Braitenberg): + + try: + port = 4000 + random.randint( + 0, 1000 + ) # use a random port to avoid problems if a previous version exits slowly + + aai_env = AnimalAIEnvironment( + inference=True, #Set true when watching the agent + seed = 123, + worker_id=random.randint(0, 65500), + file_name="../env/AnimalAI", + arenas_configurations=configuration_file, + base_port=port, + useCamera=False, + useRayCasts=True, + raysPerSide = int((agent.no_rays)/2), + rayMaxDegrees = agent.max_degrees + ) + + behavior = list(aai_env.behavior_specs.keys())[0] # by default should be AnimalAI?team=0 + + done = False + episodeReward = 0 + + aai_env.step() # take first step to get an observation + + dec, term = aai_env.get_steps(behavior) + + while not done: + + observations = aai_env.get_obs_dict(dec.obs) + + action = agent.get_action(observations) + + aai_env.set_actions(behavior, action) + + aai_env.step() + + dec, term = aai_env.get_steps(behavior) + + if len(dec.reward) > 0 and len(term) <= 0: + episodeReward += dec.reward + + elif len(term) > 0: #Episode is over + episodeReward += term.reward + print(f"Episode Reward: {episodeReward}") + done = True + + else: + pass + + aai_env.close() + except: + print("Try again. Looks like that port was busy. Sometimes it takes a while for the environment to close properly.") + + + + + + + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser() + + parser.add_argument("--config_file", + type=str, + help="What config file should be run? Defaults to a random file from the competition folder.") + + parser.add_argument("--no_rays", + type=int, + help="How many rays should the raycaster produce? Must be an odd number. Defaults to 11.", + default = 11) + parser.add_argument("--max_degrees", + type=int, + help = "Over how many degrees ought the raycasts be distributed? Defaults to 60.", + default = 60) + parser.add_argument("--verbose", + type=bool, + help = "Do you want to print out observations and actions? Defaults to False.", + default = False) + + args = parser.parse_args() + + no_rays = args.no_rays + max_degrees = args.max_degrees + verbose = args.verbose + + if args.config_file is not None: + configuration_file = args.config_file + else: + config_folder = "../configs/competition/" + configuration_files = os.listdir(config_folder) + configuration_random = random.randint(0, len(configuration_files)) + configuration_file = config_folder + configuration_files[configuration_random] + print(f"Using configuration file {configuration_file}") + + singleEpisodeVanillaBraitenberg = Braitenberg(no_rays=no_rays, + max_degrees=max_degrees, + verbose = verbose) + + watch_braitenberg_agent_single_config(configuration_file=configuration_file, agent = singleEpisodeVanillaBraitenberg) \ No newline at end of file diff --git a/agents/goToGoodBraitenberg.py b/agents/goToGoodBraitenberg.py deleted file mode 100644 index 9845654f6..000000000 --- a/agents/goToGoodBraitenberg.py +++ /dev/null @@ -1,71 +0,0 @@ -from animalai.envs.actions import AAIActions, AAIAction -from animalai.envs.raycastparser import RayCastParser -from animalai.envs.raycastparser import RayCastObjects - -class Braitenberg(): - """Implements a simple Braitenberg vehicle agent that heads towards food - Can change the number of rays but only responds to GOODGOALs, GOODGOALMULTI and BADGOAL""" - def __init__(self, no_rays): - self.no_rays = no_rays - assert(self.no_rays % 2 == 1), "Only supports odd number of rays (but environment should only allow odd number" - self.listOfObjects = [RayCastObjects.GOODGOAL, RayCastObjects.GOODGOALMULTI, RayCastObjects.BADGOAL, RayCastObjects.IMMOVABLE, RayCastObjects.MOVABLE] - self.raycast_parser = RayCastParser(self.listOfObjects, self.no_rays) - self.actions = AAIActions() - self.prev_action = self.actions.NOOP - - def prettyPrint(self, obs) -> str: - """Prints the parsed observation""" - return self.raycast_parser.prettyPrint(obs) - - def get_action(self, obs) -> AAIAction: - """Returns the action to take given the current parsed raycast observation""" - obs = self.raycast_parser.parse(obs) - newAction = self.actions.NOOP - if self.ahead(obs, RayCastObjects.GOODGOALMULTI): - newAction = self.actions.FORWARDS - elif self.left(obs, RayCastObjects.GOODGOALMULTI): - newAction = self.actions.FORWARDSLEFT - elif self.right(obs, RayCastObjects.GOODGOALMULTI): - newAction = self.actions.FORWARDSRIGHT - elif self.ahead(obs, RayCastObjects.GOODGOAL): - newAction = self.actions.FORWARDS - elif self.left(obs, RayCastObjects.GOODGOAL): - newAction = self.actions.FORWARDSLEFT - elif self.right(obs, RayCastObjects.GOODGOAL): - newAction = self.actions.FORWARDSRIGHT - elif self.ahead(obs, RayCastObjects.BADGOAL): - newAction = self.actions.BACKWARDS - elif self.left(obs, RayCastObjects.BADGOAL): - newAction = self.actions.BACKWARDSLEFT - elif self.right(obs, RayCastObjects.BADGOAL): - newAction = self.actions.BACKWARDSRIGHT - else: - if self.prev_action == self.actions.NOOP or self.prev_action == self.actions.BACKWARDS: - newAction = self.actions.LEFT - else: - newAction = self.prev_action - self.prev_action = newAction - return newAction - - def ahead(self, obs, object): - """Returns true if the input object is ahead of the agent""" - if(obs[self.listOfObjects.index(object)][int((self.no_rays-1)/2)] > 0): - # print("found " + str(object) + " ahead") - return True - return False - - def left(self, obs, object): - """Returns true if the input object is left of the agent""" - for i in range(int((self.no_rays-1)/2)): - if(obs[self.listOfObjects.index(object)][i] > 0): - # print("found " + str(object) + " left") - return True - return False - - def right(self, obs, object): - """Returns true if the input object is right of the agent""" - for i in range(int((self.no_rays-1)/2)): - if(obs[self.listOfObjects.index(object)][i+int((self.no_rays-1)/2) + 1] > 0): - # print("found " + str(object) + " right") - return True - return False \ No newline at end of file From 23d116cf27d78cfc3e19432158e698de095ae321 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 7 Sep 2023 16:42:58 -0700 Subject: [PATCH 06/10] [Minor] Change randomAc tionAgents --- agents/randomActionAgents.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agents/randomActionAgents.py b/agents/randomActionAgents.py index fab867ea7..ca1652e90 100644 --- a/agents/randomActionAgents.py +++ b/agents/randomActionAgents.py @@ -4,7 +4,7 @@ Author: Konstantinos Voudouris Date: June 2023 Python Version: 3.10.4 -Animal-AI Version: 3.0.2 +Animal-AI Version: 3.1.1 """ @@ -18,7 +18,7 @@ from animalai.envs.environment import AnimalAIEnvironment from collections import deque -from gym_unity.envs import UnityToGymWrapper +from mlagents_envs.envs.unity_gym_env import UnityToGymWrapper from scipy.special import softmax ### Random Action Agent + load config and watch. @@ -136,7 +136,7 @@ def watch_random_action_agent_single_config(configuration_file: str, agent: Rand base_port=port, useCamera=False, resolution=36, - useRayCasts=False, + useRayCasts=True, ) env = UnityToGymWrapper(aai_env, uint8_visual=False, allow_multiple_obs=True, flatten_branched=True) From 64c6971e0847f9b6f1fdff8128b4cb23d0d1e210 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 7 Sep 2023 20:01:46 -0700 Subject: [PATCH 07/10] Update random walkers and random action agents with new dependencies & tidy up slightly --- agents/randomActionAgents.py | 29 +++++++++++++++-------------- agents/randomWalkers.py | 22 ++++++++-------------- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/agents/randomActionAgents.py b/agents/randomActionAgents.py index ca1652e90..fbe6d9799 100644 --- a/agents/randomActionAgents.py +++ b/agents/randomActionAgents.py @@ -167,23 +167,24 @@ def watch_random_action_agent_single_config(configuration_file: str, agent: Rand obs=env.reset() env.close() break + + if not done: + ## get new action for one step before repeating while loop. - ## get new action for one step before repeating while loop. - - action = agent.get_new_action(prev_step = previous_action) - - obs, reward, done, info = env.step(int(action)) - - episodeReward += reward - env.render() + action = agent.get_new_action(prev_step = previous_action) + + obs, reward, done, info = env.step(int(action)) + + episodeReward += reward + env.render() - previous_action = action + previous_action = action - if done: - print(F"Episode Reward: {episodeReward}") - obs=env.reset() - env.close() - break #to be sure. + if done: + print(F"Episode Reward: {episodeReward}") + obs=env.reset() + env.close() + break #to be sure. diff --git a/agents/randomWalkers.py b/agents/randomWalkers.py index 55a8df4b7..a9afbf5d8 100644 --- a/agents/randomWalkers.py +++ b/agents/randomWalkers.py @@ -17,7 +17,7 @@ from animalai.envs.environment import AnimalAIEnvironment from collections import deque -from gym_unity.envs import UnityToGymWrapper +from mlagents_envs.envs.unity_gym_env import UnityToGymWrapper ### Random Walker Agent + load config and watch. @@ -145,15 +145,14 @@ def get_num_steps_saccade(self): num_steps = abs(num_steps) #make num_steps a positive number so it only goes forwards. if num_steps > 0: #Move forwards - step_list = deque([3,0]*abs(num_steps)) # add in a stationary movement to reduce effect of momentum on next step. - - if (num_steps % 2) == 1: - step_list.append(0) + step_list = deque([3]*abs(num_steps)) + step_list.append(0) # add in a stationary movement to reduce effect of momentum on next step. + step_list.append(0) # add in a stationary movement to reduce effect of momentum on next step. elif num_steps < 0: #Move backwards - step_list = deque([6,0]*abs(num_steps)) - if (num_steps % 2) == 1: - step_list.append(0) + step_list = deque([6]*abs(num_steps)) + step_list.append(0) # add in a stationary movement to reduce effect of momentum on next step. + step_list.append(0) # add in a stationary movement to reduce effect of momentum on next step. else: raise ValueError("Saccade length is 0. Try increasing max_saccade_length.") @@ -181,7 +180,7 @@ def get_num_steps_turn(self, prev_angle_central_moment): if right: num_steps = random.randint(0, self.max_angle_steps) else: - num_steps = random.randint(0, (self.max_angle_steps * -1)) + num_steps = random.randint(0, (self.max_angle_steps)) * -1 elif self.angle_distribution == 'normal': central_moment_difference = prev_angle_central_moment - self.angle_norm_mu @@ -329,11 +328,6 @@ def watch_random_walker_single_config(configuration_file: str, agent: RandomWalk env.close() break - if done: - print(F"Episode Reward: {episodeReward}") - env.close() - break #to be sure. - From 48a302666890c3bc775b6d6174eb73562d5971db Mon Sep 17 00:00:00 2001 From: Ibrahim Alhas <65875290+alhasacademy96@users.noreply.github.com> Date: Fri, 8 Sep 2023 18:31:08 +0100 Subject: [PATCH 08/10] Update raycastparser.py Added a new test specifically for the PILLARBUTTON raycast object. Outputting test suggests the object is being detected with the dummy data. --- animalai/animalai/envs/raycastparser.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/animalai/animalai/envs/raycastparser.py b/animalai/animalai/envs/raycastparser.py index 6cc744908..dc1306c5f 100644 --- a/animalai/animalai/envs/raycastparser.py +++ b/animalai/animalai/envs/raycastparser.py @@ -153,3 +153,19 @@ def prettyPrint(self, raycast) -> str: print("Parsed Raycast for Test 3:") print(parsedRaycast) rayParser.prettyPrint(test_raycast) + + # Test 6: Mix of objects detected and not detected, including PILLARBUTTON + # Description: This test checks if the parser correctly identifies some objects including PILLARBUTTON while ignoring others. + rayParser_6 = RayCastParser( + [RayCastObjects.ARENA, RayCastObjects.PILLARBUTTON, RayCastObjects.MOVABLE], 7) + test_raycast = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0.1, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0.2, + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0.3, + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0.4, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0.5, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.6, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0] + parsedRaycast_6 = rayParser_6.parse(test_raycast) + print("Parsed Raycast for Test 6:") + print(parsedRaycast_6) + rayParser_6.prettyPrint(test_raycast) From e40f473db2aeb07c1407d2c2e7f41890dc81f58d Mon Sep 17 00:00:00 2001 From: Ibrahim Alhas <65875290+alhasacademy96@users.noreply.github.com> Date: Fri, 8 Sep 2023 18:36:46 +0100 Subject: [PATCH 09/10] Update raycastparser.py --- animalai/animalai/envs/raycastparser.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/animalai/animalai/envs/raycastparser.py b/animalai/animalai/envs/raycastparser.py index 19d3b98e6..62a64db26 100644 --- a/animalai/animalai/envs/raycastparser.py +++ b/animalai/animalai/envs/raycastparser.py @@ -111,7 +111,7 @@ def prettyPrint(self, raycast) -> str: if isinstance(raycast, dict): raycast = raycast['rays'] - + parsedRaycast = self.parse(raycast) for i in range(parsedRaycast.shape[0]): print(self.listOfObjects[i].name, ":", parsedRaycast[i]) @@ -160,9 +160,10 @@ def prettyPrint(self, raycast) -> str: print("Parsed Raycast for Test 3:") print(parsedRaycast) rayParser.prettyPrint(test_raycast) - # Test 6: Mix of objects detected and not detected, including PILLARBUTTON + + # Test 4: Mix of objects detected and not detected, including PILLARBUTTON # Description: This test checks if the parser correctly identifies some objects including PILLARBUTTON while ignoring others. - rayParser_6 = RayCastParser( + rayParser = RayCastParser( [RayCastObjects.ARENA, RayCastObjects.PILLARBUTTON, RayCastObjects.MOVABLE], 7) test_raycast = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0.1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0.2, @@ -171,7 +172,7 @@ def prettyPrint(self, raycast) -> str: 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0.5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0] - parsedRaycast_6 = rayParser_6.parse(test_raycast) - print("Parsed Raycast for Test 6:") - print(parsedRaycast_6) - rayParser_6.prettyPrint(test_raycast) + parsedRaycast = rayParser.parse(test_raycast) + print("Parsed Raycast for Test 4:") + print(parsedRaycast) + rayParser.prettyPrint(test_raycast) From b0a700b81c2db5d45f68298e858c4e20b8b88904 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 8 Sep 2023 18:35:16 -0700 Subject: [PATCH 10/10] Add specific pandas/numpy/scipy dependencies to setup --- animalai/setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/animalai/setup.py b/animalai/setup.py index dde5e478a..30dc7ac18 100644 --- a/animalai/setup.py +++ b/animalai/setup.py @@ -18,6 +18,9 @@ ], packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), zip_safe=False, - install_requires=["mlagents==0.30.0"], + install_requires=["mlagents==0.30.0", + "numpy==1.21.2", + "scipy==1.7.2", + "pandas== 1.3.2"], python_requires=">=3.6", ) \ No newline at end of file