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 diff --git a/agents/randomActionAgents.py b/agents/randomActionAgents.py index fab867ea7..fbe6d9799 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) @@ -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. - 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/animalai/envs/raycastparser.py b/animalai/animalai/envs/raycastparser.py index d138801eb..62a64db26 100644 --- a/animalai/animalai/envs/raycastparser.py +++ b/animalai/animalai/envs/raycastparser.py @@ -1,10 +1,18 @@ import enum import numpy as np -"""Parses the raycast observations from AnimalAI and returns a shortened version with only relevant objects""" + +""" +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): - """Enum for the parsed objects from the raycast. The order must match the order in the Unity environment.""" + """ + 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 @@ -12,63 +20,77 @@ 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. + + Parameters: + - raycast: The raw raycast array from the AnimalAI environment. - print(f"Initial raycast: {raycast}") - print(f"List of Objects: {self.listOfObjects}") + Returns: + - np.ndarray: The parsed raycast, simplified to only include objects in listOfObjects. + """ + if isinstance(raycast, dict): + raycast = raycast['rays'] - assert (len(raycast) == self.numberOfRays * - (self.numberDetectableObjects+2)) + 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 +100,79 @@ 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. + """ + + 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]) 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]]))) + 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.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]]))) + [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) + 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.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]]))) - 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]) + 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) - 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 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 = 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.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 = rayParser.parse(test_raycast) + print("Parsed Raycast for Test 4:") print(parsedRaycast) + rayParser.prettyPrint(test_raycast) diff --git a/animalai/setup.py b/animalai/setup.py index 77d245efb..30dc7ac18 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,9 +14,13 @@ "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, - 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