diff --git a/week2/solution/agent_programs.py b/week2/solution/agent_programs.py new file mode 100644 index 0000000000000000000000000000000000000000..b53fd00f696e8c62e31822b81fbed3eb5d687029 --- /dev/null +++ b/week2/solution/agent_programs.py @@ -0,0 +1,303 @@ +import math +import random +import numpy as np + +from une_ai.vacuum import DISPLAY_HEIGHT, DISPLAY_WIDTH, TILE_SIZE +from une_ai.models import GridMap +from vacuum_agent import VacuumAgent + +DIRECTIONS = VacuumAgent.WHEELS_DIRECTIONS + +""" +Test agent: +- If the vacuum power is off, it starts cleaning +- At each time, it chooses a random direction for the wheels +""" +def test_behaviour(percepts, actuators): + actions = [] + + # if the power is off, we start cleaning + if actuators['vacuum-power'] != 1: + actions.append('start-cleaning') + + # we choose a new random direction + new_direction = random.choice(DIRECTIONS) + actions.append('change-direction-{0}'.format(new_direction)) + + return actions + +""" +Simple reflex agent: +- If the vacuum power is off, it starts cleaning +- If there is dirt on the current tile (i.e. 'dirt-sensor-center'), +it activates the suction mechanism +- If the agent hits a wall, it changes the direction of the wheels randomly +- If the agent senses dirt on the surrounding tiles, +it changes the direction of the wheels towards the dirt +""" +def simple_reflex_behaviour(percepts, actuators): + actions = [] + + # if the power is off we start cleaning + if actuators['vacuum-power'] != 1: + actions.append('start-cleaning') + + # if there is dirt, we activate the suction mechanism + if percepts['dirt-sensor-center'] == True: + actions.append('activate-suction-mechanism') + elif actuators['suction-power'] == 1: + # if not and the suction mechanism is on, we shut it down to conserve battery + actions.append('deactivate-suction-mechanism') + + cur_direction = actuators['wheels-direction'] + new_direction = cur_direction + # if we bumped into a wall we change direction + if percepts['bumper-sensor-{0}'.format(cur_direction)] == True: + directions = DIRECTIONS.copy() + # we remove the current direction from the list + directions.remove(cur_direction) + new_direction = random.choice(directions) + + # we look if there is dirt in the adjacent cells + for dir in DIRECTIONS: + if percepts['dirt-sensor-{0}'.format(dir)] == True: + # there is dirt, that's a better direction to move to + new_direction = dir + break + + # if we changed direction, we add an action to change it + if new_direction != cur_direction: + actions.append('change-direction-{0}'.format(new_direction)) + + return actions + +""" +Model-based reflex agent: +- The agent keeps track of the walls it crashed against by using a GridMap +- Based on the current wheels direction, if the next tile is a wall, +the agent will change direction +- In all the other situations, the agent will behave like the simple-reflex agent +""" + +w_env = int(DISPLAY_WIDTH/ TILE_SIZE) +h_env = int(DISPLAY_HEIGHT/TILE_SIZE) +environment_map = GridMap(w_env, h_env, None) + +def future_state(model, cur_location, direction): + offset = { + 'north': (0, -1), + 'south': (0, 1), + 'west': (-1, 0), + 'east': (1, 0) + } + cur_x, cur_y = cur_location + new_x, new_y = (cur_x + offset[direction][0], cur_y + offset[direction][1]) + + try: + value = model.get_item_value(new_x, new_y) + new_location = (new_x, new_y) + except: + # if here it means that the next location will be out of bounds + # so that's a wall + value = 'W' + new_location = None + + return value, new_location + +def model_based_reflex_behaviour(percepts, actuators): + # we can start from the actions determined by the simple reflex agent + actions = simple_reflex_behaviour(percepts, actuators) + + # if there was a collision, I need to update the model + cur_direction = actuators['wheels-direction'] + agent_location = percepts['location-sensor'] + if percepts['bumper-sensor-{0}'.format(cur_direction)] == True: + _, future_location = future_state(environment_map, agent_location, cur_direction) + if future_location is not None: + environment_map.set_item_value(future_location[0], future_location[1], 'W') + + # we check if among the actions + # selected by the simple-reflex behaviour + # there is one to change direction + new_direction = cur_direction + for action in actions: + if action.startswith('change-direction'): + # this means that we hit a wall or there is dirt around the agent + # we save this as future direction + tokens = action.split('-') + new_direction = tokens[2] + # and remove it from the actions + actions.remove(action) + + # we need to check the adjacent cells for walls + valid_directions = [] + for direction in DIRECTIONS: + future_state_value, _ = future_state(environment_map, agent_location, direction) + if future_state_value != 'W': + valid_directions.append(direction) + + # now we can check if the new direction is among the + # valid directions with no walls + # if not, we need to change it with a valid one + if new_direction not in valid_directions: + new_direction = random.choice(valid_directions) + + # if we changed direction, we add an action to change it + if new_direction != cur_direction: + actions.append('change-direction-{0}'.format(new_direction)) + + return actions + +""" +Goal-based agent: +- The agent keeps track of previously explored tiles by using a GridMap +- Based on the current wheels direction, if the next tile was already explored, +the agent will change direction towards an unexplored tile (if any, otherwise +it will proceed in the same direction) +- In all the other situations, the agent will behave like the model-based reflex agent +- The agent will stop cleaning once the environment is fully explored +""" +def goal_based_behaviour(percepts, actuators): + # we can start from the actions determined by the model-based reflex agent + # doing this will also update the map with walls + actions = model_based_reflex_behaviour(percepts, actuators) + + # we can also update the current cell as visited + agent_location = percepts['location-sensor'] + environment_map.set_item_value(agent_location[0], agent_location[1], 'X') + + # we check if among the actions + # selected by the model-based reflex behaviour + # there is one to change direction + cur_direction = actuators['wheels-direction'] + new_direction = cur_direction + for action in actions: + if action.startswith('change-direction'): + # this means that we hit a wall, + # there is dirt around the agent, + # or there is a wall towards us + # we save this as future direction + tokens = action.split('-') + new_direction = tokens[2] + # and remove it from the actions + actions.remove(action) + + # we determine the valid directions + # with previously unexplored tiles + valid_directions = [] + for direction in DIRECTIONS: + future_state_value, _ = future_state(environment_map, agent_location, direction) + if future_state_value is None: + valid_directions.append(direction) + + # we check if the new direction is among the valid ones + # and that valid_directions is not empty + # if not, we change it + if len(valid_directions) > 0 and new_direction not in valid_directions: + # we change direction + new_direction = random.choice(valid_directions) + + # if we changed direction, we add an action to change it + if new_direction != cur_direction: + actions.append('change-direction-{0}'.format(new_direction)) + + # if we visited all the environment, we shut down the power + if len(environment_map.find_value(None)) == 0: + actions.append('stop-cleaning') + + return actions + +""" +Utility-based agent: +The agent also stores information about dirt on the adjacent cells detected by the dirt sensors. +The agent then chooses the next direction via a utility function. +This utility function takes a direction as input, and implement the following steps: +- The agent examines its internal model of the world and retrieves a list of cell values +in the specified direction. +- It filters out any cells that are obstructed by a wall, considering only the unobstructed cells. +- If there is dirt in the considered direction, the utility is returned as a high value such as 999 +otherwise +- The agent calculates the minimum distance (min_dist) from an unexplored cell in this +filtered list. If there are no unexplored cells, min_dist is set to a high value such as 999. +- The utility value is determined as -1 multiplied by min_dist, +reflecting the notion that the agent values smaller distances to unexplored cells. +""" +def utility_function(model, cur_location, direction): + x, y = cur_location + # take the cells in the given direction + if direction == 'north': + cells = model.get_column(x) + cells = np.flip(cells[0:y]) + elif direction == 'south': + cells = model.get_column(x) + cells = cells[y+1:] + elif direction == 'west': + cells = model.get_row(y) + cells = np.flip(cells[0:x]) + elif direction == 'east': + cells = model.get_row(y) + cells = cells[x+1:] + else: + cells = [] + + # remove the cells obstructed by a wall + filtered_cells = [] + for cell in cells: + if cell != 'W': + filtered_cells.append(cell) + else: + # wall + break + + # check if there is dirt in that direction + for cell in cells: + if cell == 'D': + # there is dirt, return high utility + return 999 + + # compute the min distance from unexplored cells + min_dist = 999 + i = 0 + for cell in filtered_cells: + if cell is None: + min_dist = i + break + i += 1 + + # return the utility as -1*min_dist + return -1*min_dist + +def utility_based_behaviour(percepts, actuators): + # we can start from the actions determined by the goal-based agent + # doing this will also update the map with walls and explored cells + actions = goal_based_behaviour(percepts, actuators) + + # we update the environment map with information about the dirt on adjacent cells + agent_location = percepts['location-sensor'] + for direction in DIRECTIONS: + if percepts['dirt-sensor-{0}'.format(direction)] == True: + _, new_location = future_state(environment_map, agent_location, direction) + environment_map.set_item_value(new_location[0], new_location[1], 'D') + + # we remove from the actions any change of direction + # as we determine the best direction based on the utility function + for action in actions: + if action.startswith('change-direction'): + actions.remove(action) + + # we determine the best direction + cur_direction = actuators['wheels-direction'] + max_value = None + best_dir = None + for direction in DIRECTIONS: + cur_utility = utility_function(environment_map, agent_location, direction) + if max_value is None or cur_utility > max_value: + max_value = cur_utility + best_dir = direction + + # if we changed direction, we add an action to change it + if best_dir != cur_direction: + actions.append('change-direction-{0}'.format(best_dir)) + + return actions + \ No newline at end of file diff --git a/week2/solution/code/agent_programs.py b/week2/solution/code/agent_programs.py deleted file mode 100644 index c268416e121087832258085b705caa75d175fa9c..0000000000000000000000000000000000000000 --- a/week2/solution/code/agent_programs.py +++ /dev/null @@ -1,271 +0,0 @@ -import math -import random -import numpy as np - -from une_ai.vacuum import VacuumAgent, DISPLAY_HEIGHT, DISPLAY_WIDTH, TILE_SIZE -from une_ai.models import GridMap - -DIRECTIONS = VacuumAgent.WHEELS_DIRECTIONS - -""" -Simple reflex agent: -- If the vacuum is on, turn it on -- If there is dirt on the current tile, activate the suction mechanism -- If the agent hit a wall, change the direction of the wheels randomly -- If the agent senses dirt on the surrounding tiles, change the direction of the wheels towards the dirt -""" -def simple_reflex_behaviour(percepts, actuators): - actions = [] - - # if the vacuum is off, turn it on - if actuators['vacuum-power'] == 0: - actions.append('start-cleaning') - - # if there is dirt on the current tile, clean it - if percepts['dirt-sensor-center']: - actions.append('activate-suction-mechanism') - else: - actions.append('deactivate-suction-mechanism') - - # if the agent hit a wall, change direction - for direction in DIRECTIONS: - if percepts['bumper-sensor-{0}'.format(direction)] == True: - new_direction = actuators['wheels-direction'] - while new_direction == actuators['wheels-direction']: - new_direction = random.choice(DIRECTIONS) - actions.append('change-direction-{0}'.format(new_direction)) - - # if there is dirt on the surronding tiles, move in that direction - for direction in DIRECTIONS: - if percepts['dirt-sensor-{0}'.format(direction)]: - actions.append('change-direction-{0}'.format(direction)) - break - - return actions - -""" -Model-based reflex agent: -- The agent keeps track of the explored tiles -- If the environment is fully explored, then turn off -- ELSE -- behave like the simple-reflex agent -""" -w_env = math.floor(DISPLAY_WIDTH / TILE_SIZE) -h_env = math.floor(DISPLAY_HEIGHT / TILE_SIZE) -explored_map = GridMap(w_env, h_env) - -# compute offsets for the next movement -movement_offsets = { - 'north': (0, -1), - 'south': (0, 1), - 'west': (-1, 0), - 'east': (1, 0) -} - -def model_based_reflex_behaviour(percepts, actuators): - - # stopping when the whole environment is explored - n_unexplored_tiles = len(explored_map.find_value(False)) - if n_unexplored_tiles == 0: - return ['stop-cleaning'] - - # if here, it means that there are unexplored tiles - - # the actions are the same of the simple-reflex agent - actions = simple_reflex_behaviour(percepts, actuators) - - # we also need to update the model of the environment - agent_x, agent_y = percepts['location-sensor'] - try: - explored_map.set_item_value(agent_x, agent_y, True) - except: - # out of bounds, no recordings on the map - pass - - # checking if the agent bumped into a wall and update - # the map accordingly (as the agent did not move there) - present_direction = actuators['wheels-direction'] - offset_x, offset_y = movement_offsets[present_direction] - next_x, next_y = (agent_x + offset_x, agent_y + offset_y) - for direction in DIRECTIONS: - if percepts['bumper-sensor-{0}'.format(direction)] == True: - try: - explored_map.set_item_value(next_x, next_y, True) - except: - # out of bounds, that's ok - pass - - return actions - -""" -Goal-based agent: -- We keep track of the explored tiles -- The maps are used to predict if the next movement will lead to a previously explored tile and... -... If so, the agent changes direction towards an unexplored tile -ELSE -- the agent behaves like the model-based reflex agent -""" -def goal_based_reflex_behaviour(percepts, actuators): - # start with the actions from the model-based reflex agent - # this will also update the model of the environment - actions = model_based_reflex_behaviour(percepts, actuators) - - # if there is the action stop-cleaning - # it means we visited the whole environment - if 'stop-cleaning' in actions: - return actions - - # else, we check if we are going in a direction with an unexplored tile - chosen_direction = None - for action in actions: - if action.startswith('change-direction-'): - chosen_direction = action.split('-')[2] - - if chosen_direction is None: - chosen_direction = actuators['wheels-direction'] - - # making predictions about the future - # to check if it aligns with our goal of cleaning - # the whole environment - first_option = chosen_direction - direction_found = False - i = 0 - directions = DIRECTIONS.copy() - # shuffle the directions so to simulate a random choice - random.shuffle(directions) - agent_x, agent_y = percepts['location-sensor'] - while not direction_found: - offset_x, offset_y = movement_offsets[chosen_direction] - new_x, new_y = (agent_x + offset_x, agent_y + offset_y) - - try: - is_explored = explored_map.get_item_value(new_x, new_y) - except: - # out of bounds, set it as explored - is_explored = True - - if is_explored: - # we already visited the next tile - # change direction with the next one - if i < len(directions): - chosen_direction = directions[i] - i += 1 - # and try again - else: - # it seems that everything was visited - # we go with the first option we got - chosen_direction = first_option - break - else: - # we found an unvisited tile - direction_found = True - - # append the action, only the last change in the list will take place - actions.append('change-direction-{0}'.format(chosen_direction)) - - return actions - -""" -Utility-based reflex agent: -We behave exactly as the goal-based agent but we try to work more efficiently, so -- Behave as the goal-based agent -- IF the chosen direction leads to a previously explored tile it means that the agent explored all the surroundings... -... so we choose a direction leading to the closer unexplored tile that is not obstructed by a wall -""" - -# for this agent we also need to keep track of the walls -walls_map = GridMap(w_env, h_env) - -def find_best_direction(x, y): - r = explored_map.get_row(y) - c = explored_map.get_column(x) - rw = walls_map.get_row(y) - cw = walls_map.get_column(x) - tiles_by_dir = { - 'east': (r[x+1:], rw[x+1:]), - 'west': (np.flip(r[:x]), np.flip(rw[:x])), - 'north': (np.flip(c[:y]), np.flip(cw[:y])), - 'south': (c[y+1:], cw[y+1:]) - } - min_dist = None - min_dist_dir = None - for direction in DIRECTIONS: - cur_dist = None - # check if there is an unexplored tile towards this direction - try: - cur_dist = min(np.argwhere(tiles_by_dir[direction][0] == False))[0] - except: - # no empty tiles in this direction, skip - continue - - # if we are here it means that there is an unexplored tile - # towards this direction, let's see if it is unobstructed - wall_dist = None - try: - wall_dist = min(np.argwhere(tiles_by_dir[direction][1] == True))[0] - except: - # there are no walls, wall_dist remains to None - pass - - if wall_dist is not None and cur_dist > wall_dist: - # unexplored tile directly not reachable, skip - continue - - # computing the min distance - if min_dist is None or cur_dist < min_dist: - min_dist = cur_dist - min_dist_dir = direction - - return min_dist_dir - -def utility_based_reflex_behaviour(percepts, actuators): - agent_x, agent_y = percepts['location-sensor'] - - # we start behaving like the goal-based agent - actions = goal_based_reflex_behaviour(percepts, actuators) - - # checking if the agent bumped into a wall during the previous - # iteration and update the walls map accordingly - present_direction = actuators['wheels-direction'] - offset_x, offset_y = movement_offsets[present_direction] - next_x, next_y = (agent_x + offset_x, agent_y + offset_y) - for direction in DIRECTIONS: - if percepts['bumper-sensor-{0}'.format(direction)] == True: - try: - walls_map.set_item_value(next_x, next_y, True) - except: - # out of bounds, nothing to record - pass - - # now we check the chosen new direction - # if it leads to a previously explored tile, - # it means that the goal-based agent policy did not find - # any new surrounding tile to explore - chosen_direction = actuators['wheels-direction'] - for action in actions: - if action.startswith('change-direction-'): - chosen_direction = action.split('-')[2] - - offset_x, offset_y = movement_offsets[chosen_direction] - next_x, next_y = (agent_x + offset_x, agent_y + offset_y) - - try: - is_explored = explored_map.get_item_value(next_x, next_y) - except: - # out of bounds, set it as explored - is_explored = True - - new_best_dir = chosen_direction - if is_explored: - # We did not find any unvisited tile in the surroundings - # we need to find the best direction to go leading to the closer unobstructed unexplored tile - new_best_dir = find_best_direction(agent_x, agent_y) - if new_best_dir is None: - # this may happen when in a well cleaned area, let's re-use the direction from the previous agent - new_best_dir = chosen_direction - - # append it, if there is more than a change direction action, only the last in the list will take place - actions.append('change-direction-{0}'.format(new_best_dir)) - - return actions - \ No newline at end of file diff --git a/week2/solution/code/vacuum_world.py b/week2/solution/code/vacuum_world.py deleted file mode 100644 index 94e83d0e9371e7a3a050df60f22a02da1a140569..0000000000000000000000000000000000000000 --- a/week2/solution/code/vacuum_world.py +++ /dev/null @@ -1,6 +0,0 @@ -from une_ai.vacuum import VacuumGame, DISPLAY_HEIGHT, DISPLAY_WIDTH -from agent_programs import simple_reflex_behaviour, model_based_reflex_behaviour, goal_based_reflex_behaviour, utility_based_reflex_behaviour - -if __name__ == "__main__": - # To test the different agent programs, change the function passed as parameter to create the instance of VacuumGame - game = VacuumGame(utility_based_reflex_behaviour, DISPLAY_WIDTH, DISPLAY_HEIGHT) \ No newline at end of file diff --git a/week2/solution/vacuum_agent.py b/week2/solution/vacuum_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..3897e62d20d3fa71f883f91bc363a4b5281e560e --- /dev/null +++ b/week2/solution/vacuum_agent.py @@ -0,0 +1,90 @@ +from une_ai.models import Agent + +class VacuumAgent(Agent): + + WHEELS_DIRECTIONS = ['north', 'south', 'west', 'east'] + + def __init__(self, agent_program): + super().__init__( + agent_name='vacuum_agent', + agent_program=agent_program + ) + + def add_all_sensors(self): + self.add_sensor('battery-level', 0, lambda v: isinstance(v, float) or isinstance(v, int) and v >= 0) + self.add_sensor('location-sensor', (0, 0), lambda v: isinstance(v, tuple) and isinstance(v[0], int) and isinstance(v[1], int)) + + directions = VacuumAgent.WHEELS_DIRECTIONS.copy() + directions.append('center') + for direction in directions: + self.add_sensor('dirt-sensor-{0}'.format(direction), False, lambda v: v in [True, False]) + if direction != 'center': + self.add_sensor('bumper-sensor-{0}'.format(direction), False, lambda v: v in [True, False]) + + def add_all_actuators(self): + self.add_actuator( + 'wheels-direction', + 'north', + lambda v: v in VacuumAgent.WHEELS_DIRECTIONS + ) + self.add_actuator( + 'vacuum-power', + 0, + lambda v: v in [0, 1] + ) + self.add_actuator( + 'suction-power', + 0, + lambda v: v in [0, 1] + ) + + def add_all_actions(self): + self.add_action( + 'start-cleaning', + lambda: {'vacuum-power': 1} if not self.is_out_of_charge() else {} + ) + self.add_action( + 'stop-cleaning', + lambda: { + 'vacuum-power': 0 + } + ) + self.add_action( + 'activate-suction-mechanism', + lambda: {'suction-power': 1} if not self.is_out_of_charge() else {} + ) + self.add_action( + 'deactivate-suction-mechanism', + lambda: { + 'suction-power': 0 + } + ) + for direction in VacuumAgent.WHEELS_DIRECTIONS: + self.add_action( + 'change-direction-{0}'.format(direction), + lambda d=direction: {'wheels-direction': d} if not self.is_out_of_charge() else {} + ) + + def get_pos_x(self): + return self.read_sensor_value('location-sensor')[0] + + def get_pos_y(self): + return self.read_sensor_value('location-sensor')[1] + + def is_out_of_charge(self): + return self.read_sensor_value('battery-level') == 0 + + def get_battery_level(self): + return int(self.read_sensor_value('battery-level')) + + def collision_detected(self): + directions = VacuumAgent.WHEELS_DIRECTIONS.copy() + for direction in directions: + bumper = self.read_sensor_value('bumper-sensor-{0}'.format(direction)) + if bumper: + return direction + + return None + + def did_collide(self): + return False if self.collision_detected() is None else True \ No newline at end of file diff --git a/week2/solution/vacuum_app.py b/week2/solution/vacuum_app.py new file mode 100644 index 0000000000000000000000000000000000000000..5e5c5cfed62f4563208ac01c80eec0979c12f10a --- /dev/null +++ b/week2/solution/vacuum_app.py @@ -0,0 +1,13 @@ +from une_ai.vacuum import VacuumGame +from vacuum_agent import VacuumAgent +from agent_programs import test_behaviour, simple_reflex_behaviour, model_based_reflex_behaviour, goal_based_behaviour, utility_based_behaviour + +if __name__ == "__main__": + # creating the vacuum agent + # To test the different agent programs, change the function passed + # as parameter when instantiating the class VacuumAgent + agent = VacuumAgent(utility_based_behaviour) + + # running the game with the instantiated agent + # DO NOT EDIT THIS INSTRUCTION! + game = VacuumGame(agent) \ No newline at end of file