diff --git a/cs2109s/labs/ps2/cube.py b/cs2109s/labs/ps2/cube.py new file mode 100644 index 0000000..e60223c --- /dev/null +++ b/cs2109s/labs/ps2/cube.py @@ -0,0 +1,295 @@ +""" +A Rubik's Cube like this + 0 1 2 + |-----|-----|-----| + 0 | R | R | R | + |-----|-----|-----| + 1 | G | G | G | + |-----|-----|-----| + 2 | B | B | B | + |-----|-----|-----| +""" +import copy +import json +from ast import literal_eval +from typing import Dict, Iterable, List, Optional, Tuple, Union + +Action = List[Union[int, str]] + +class State: + r"""State class describes the setting of the Cube + + Args: + shape (List[int]): describe the number of rows and columns of the cube + layout (Iterable[int]): describe the layout of the cube. The length of + layout should be equal to the product of shape. + + Example: + State([2,3],[0, 1, 2, 3, 4, 5]) represents the state of + label: 0 1 2 + 0 | 0 | 1 | 2 | + 1 | 3 | 4 | 5 | + + Methods: + left(label): move the @label row left + returns the copy of new state (State) + + right(label): move the @label row right + returns the copy of new state (State) + + up(label): move the @label col up + returns the copy of new state (State) + + down(label): move the @label col down + returns the copy of new state (State) + """ + + def __init__(self, shape: Tuple[int,int], layout: Iterable[int]): + if len(layout) != shape[0]*shape[1]: + raise ValueError("layout does not match the shape") + self.__shape = list(shape) + self.__layout = layout + + def __eq__(self, state: "State"): + if isinstance(state, State): + same_shape = state.shape[0] == self.__shape[0] and \ + state.shape[1] == self.__shape[1] + same_layout = all([x==y for x,y in zip(self.__layout,state.layout)]) + return same_shape and same_layout + else: + return False + + def __hash__(self) -> int: + return hash(tuple(self.__layout)) + + def __repr__(self) -> str: + return str({'shape': self.__shape, 'layout': self.__layout}) + + def __str__(self): + # Header + row_str = f"{' '*5} " + for col in range(self.shape[1]): + row_str += f"{col:^5d} " + cube_str = row_str + '\n' + cube_str += f"{' '*5}+{'-----+'*self.shape[1]}\n" + # Content + for row in range(self.shape[0]): + row_str = f"{row:^5d}|" + for col in range(self.shape[1]): + row_str += f"{str(self.layout[row*self.shape[1]+col]):^5s}|" + cube_str += row_str + '\n' + cube_str += f"{' '*5}+{'-----+'*self.shape[1]}\n" + + return cube_str + + + @property + def shape(self): + return copy.deepcopy(self.__shape) + + @property + def layout(self): + return copy.deepcopy(self.__layout) + + def left(self, label): + layout = self.layout + rows, cols = self.shape + head = layout[label * cols] + for i in range(cols-1): + layout[label * cols + i] = layout[label * cols + i + 1] + layout[(label+1) * cols - 1] = head + return State(self.shape,layout) + + def right(self, label): + layout = self.layout + rows, cols = self.shape + tail = layout[(label + 1) * cols - 1] + for i in range(cols - 1, 0, -1): + layout[label * cols + i] = layout[label * cols + i - 1] + layout[label * cols] = tail + return State(self.shape,layout) + + def up(self, label): + layout = self.layout + rows, cols = self.shape + head = layout[label] + for i in range(rows-1): + layout[label + cols * i] = layout[label + cols * (i + 1)] + layout[label + cols * (rows - 1)] = head + return State(self.shape,layout) + + def down(self, label): + layout = self.layout + rows, cols = self.shape + tail = layout[label + cols * (rows - 1)] + for i in range(rows - 1, 0, -1): + layout[label + cols * i] = layout[label + cols * (i - 1)] + layout[label] = tail + return State(self.shape,layout) + + +class Cube: + r"""Cube problem class + Args: + input_file (Optional[str]): the absolute path of the Cube json file + initial (Optional[State]): the initial state of the Cube + goal (Union[State, Iterable[State]]): the goal + state(s) of the cube. + """ + def __init__( + self, + input_file: Optional[str] = None, + input_dict: Optional[Dict] = None, + initial: Optional[State] = None, + goal:Optional[State] = None + ): + if input_file: + with open(input_file, 'r') as f: + data = json.load(f) + state_dict = literal_eval(data['initial']) + self.__initial = State(state_dict['shape'],state_dict['layout']) + state_dict = literal_eval(data['goal']) + self.__goal = State(state_dict['shape'],state_dict['layout']) + + elif input_dict: + state_dict = input_dict['initial'] + self.__initial = State(state_dict['shape'],state_dict['layout']) + state_dict = input_dict['goal'] + self.__goal = State(state_dict['shape'],state_dict['layout']) + + elif all([initial, goal]): + self.__initial = initial + self.__goal = goal + + else: + raise ValueError + + self.__actions = self._get_actions(*self.__initial.shape) + + def __repr__(self) -> str: + return repr({'initial':repr(self.__initial), 'goal':repr(self.__goal)}) + + def __str__(self) -> str: + return f"initial:\n{str(self.__initial)}\ngoal:\n{str(self.__goal)}" + + def _get_actions(self, rows:int, cols:int): + actions = [] + for i in range(rows): + actions.append([i,"left"]) + actions.append([i,"right"]) + for i in range(cols): + actions.append([i,"up"]) + actions.append([i,"down"]) + return actions + + + # Observable Environment + @property + def initial(self): + return copy.deepcopy(self.__initial) + + @property + def goal(self): + return copy.deepcopy(self.__goal) + + def actions(self, state: State): + r"""Return the actions that can be executed in the given state. + + Args: + state (State): the state to be checked for actions. + + Returns: + A list of actions can be executed at the provided state. + """ + return copy.deepcopy(self.__actions) + + # Transition Model (Deterministic) + def result(self, source: State, action): + r"""Return the state that results from executing the given + action in the given state. The action must be one of + self.actions(state). + + Args: + source (State): the state to excute the action + action: the action can be executed + + Returns: + the state after taking action from source + """ + assert list(action) in self.actions(source), \ + f"{action} is illegal action at {source}" + + label, act = action + if act == 'left': + result = source.left(label) + elif act == 'right': + result = source.right(label) + elif act == 'down': + result = source.down(label) + elif act == 'up': + result = source.up(label) + return result + + def path_cost(self, c: float, state1: State, action, + state2: State) -> float: + r"""Return the cost of a solution path that arrives at state2 from + state1 via action, assuming cost c to get up to state1. + + .. math:: + c + action cost + + Args: + c (float): the cost of getting state1 from the initial state + state1 (State): the State before executing action + action: the action taken at state1 + state2 (State): the State after executing action + + Returns: + the path cost of reaching state2 + """ + if self.result(state1, action) == state2: + return c + 1 + + # Goal Test + def goal_test(self, state: State) -> bool: + r"""Return True if the state is a goal. The default method compares the + state to self.goal or checks for state in self.goal if it is a + list, as specified in the constructor. + + Args: + state (State): the state to be checked + + Return: + True if the give state is the goal state, otherwise False + """ + if isinstance(self.__goal, list): + return any(state == x for x in self.__goal) + else: + return state == self.__goal + + # Solution Check + def verify_solution(self, solution, _print=False): + r"""Verify whether the given solution can reach goal state + + Args: + solution (List): the list of actions is supposed to reach + goal state + + Returns: + (True, cost) if the solution can reach the goal state, + (False, cost) if the solution fails to reach the goal state. + + Notes: + cost == 0 means that there exists an illegal action in the solution + """ + curr = self.__initial + cost = 0 + for action in solution: + if _print: + print(curr, action) + if list(action) not in self.actions(curr): + return False, 0 + next = self.result(curr, action) + cost = self.path_cost(cost, curr, action, next) + curr = next + return self.goal_test(curr), cost diff --git a/cs2109s/labs/ps2/graph.png b/cs2109s/labs/ps2/graph.png new file mode 100644 index 0000000..670b26f Binary files /dev/null and b/cs2109s/labs/ps2/graph.png differ diff --git a/cs2109s/labs/ps2/ps2.ipynb b/cs2109s/labs/ps2/ps2.ipynb new file mode 100644 index 0000000..b662bfb --- /dev/null +++ b/cs2109s/labs/ps2/ps2.ipynb @@ -0,0 +1,855 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Problem Set 2: Informed Search\n", + "\n", + "**Release Date:** 30 January 2024\n", + "\n", + "**Due Date:** 23:59, 10 February 2024" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Overview\n", + "\n", + "In class, we discussed a range of different searching algorithms. In this problem set, we will get some hands-on practice by implementing them for simple logic problems. In particular, we will investigate a special 2-D Rubik’s Cube problem and the well-known Traveling Salesman Problem (TSP). Both problems operate in a fully-observable, single-agent, deterministic, episodic, static, and discrete environment.\n", + "\n", + "**Required Files**:\n", + "* cube.py\n", + "* utils.py\n", + "\n", + "**Honour Code**: Note that plagiarism will not be condoned! You may discuss with your classmates and check the Internet for references, but you MUST NOT submit code/report that is copied directly from other sources!\n", + "\n", + "**IMPORTANT**: While it is possible to write and run Python code directly in Jupyter notebook, we recommend that you do this Problem set with an IDE using the .py file provided. An IDE will make debugging significantly easier." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2-D Rubik’s Cube\n", + "“The Rubik’s Cube is a 3-D combination puzzle invented in 1974 by Hungarian sculptor and professor of architecture Erno Rubik. Rubik’s Cube won the 1980 German Game of the Year special award for Best Puzzle. As of January 2009, 350 million cubes had been sold worldwide, making it the world’s bestselling puzzle game and bestselling toy.” – Wikipedia. In this task, we explore a simplified version, 2-D Rubik’s “Cube”. To help you understand A* search, you will design and implement an A* search algorithm to find the solution of any 2D cube.\n", + "\n", + "**Please take note that the \"cube\" is rectangular and can be of any shape `[rows, columns]`, where rows, columns can be any positive integer**. \n", + "\n", + "For demonstration, we take a standard cube of shape 3 rows × 3 columns as an example to explain the rule of the game. Given any initial configuration of the cube, we are interested in finding a sequence of moves that leads the cube to be in a predefined goal configuration in the **least** number of steps. \n", + "\n", + "In the following example, an initial configuration of the cube is `[[R, G, B], [R, G, B], [R, G, B]]` and we are interested in taking the least possible number of actions to reach the predefined goal configuration `[[R, R, R], [G, G, G], [B, B, B]]`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{align*}\n", + "initial:\n", + "\\begin{bmatrix}\n", + " R & G & B \\\\\n", + " R & G & B \\\\\n", + " R & G & B \n", + "\\end{bmatrix}\n", + "& \\qquad goal:\n", + "\\begin{bmatrix}\n", + " R & R & R \\\\\n", + " G & G & G \\\\\n", + " B & B & B \n", + "\\end{bmatrix}\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "On each move, we can pick a **number** and a **direction** to manipulate the cube, i.e. select a row number and a horizontal move direction (left/right), or select a column number and a vertical move direction (up/down). Each move will only change the elements in the selected row/column, leaving the rest of the cube unchanged. For example, if row **1** and move direction **left** are picked, all elements in row 1 will be shifted to the left with the leftmost element re-emerging on the rightmost column of the same row and the rest of the rows unchanged:" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{array}{rcccc}\n", + "\\begin{matrix}\n", + " 0 \\\\\n", + " 1 \\\\\n", + " 2\n", + " \\end{matrix}\n", + " & \n", + " \\begin{bmatrix}\n", + " R & G & B \\\\\n", + " R & G & B \\\\\n", + " R & G & B \n", + " \\end{bmatrix}\n", + " &\n", + " \\Rightarrow\n", + " & \n", + " \\begin{bmatrix}\n", + " R & G & B \\\\\n", + " \\textbf{G} & \\textbf{B} & \\textbf{R} \\\\\n", + " R & G & B \n", + " \\end{bmatrix}\n", + "\\end{array}\n", + "$$" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the effect of a move is circular and therefore consecutively moving the cube on the same row/column and direction twice is the same as moving the cube on the same row/column in the opposite direction once in a 3-by-3 cube. We encourage you to play with this cube to discover more insights and useful rules.\n", + "\n", + "Here we provide a simple solution for the above example. You can walk through this solution step by step to get a better understanding of this problem." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{array}{rccccc}\n", + "& 0 & 1 & 2 & 3 & 4 \\\\\n", + "\\begin{matrix}\n", + " 0 \\\\\n", + " 1 \\\\\n", + " 2 \n", + " \\end{matrix}\n", + " & \n", + " \\begin{bmatrix}\n", + " R & G & B \\\\\n", + " R & G & B \\\\\n", + " R & G & B \n", + " \\end{bmatrix}\n", + " & \n", + " \\begin{bmatrix}\n", + " R & G & B \\\\\n", + " G & B & R \\\\\n", + " R & G & B \n", + " \\end{bmatrix}\n", + " & \n", + " \\begin{bmatrix}\n", + " R & G & B \\\\\n", + " G & B & R \\\\\n", + " B & R & G \n", + " \\end{bmatrix}\n", + " & \n", + " \\begin{bmatrix}\n", + " R & R & B \\\\\n", + " G & G & R \\\\\n", + " B & B & G \n", + " \\end{bmatrix}\n", + " & \n", + " \\begin{bmatrix}\n", + " R & R & R \\\\\n", + " G & G & G \\\\\n", + " B & B & B \n", + " \\end{bmatrix} \\\\\n", + " & (1, left) & (2, right) & (1, down) & (2, up) &\n", + "\\end{array}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "*Please run the following cell before proceeding. You may use any of the imported libraries/classes here.*" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import copy\n", + "import heapq\n", + "import math\n", + "import os\n", + "import random\n", + "import sys\n", + "import time\n", + "\n", + "import utils\n", + "import cube\n", + "\n", + "from typing import List\n", + "from typing import Tuple\n", + "\n", + "# For following test cases\n", + "def wrap_test(func):\n", + " def inner(*args, **kwargs):\n", + " try:\n", + " return func(*args, **kwargs)\n", + " except Exception as e:\n", + " return f'FAILED, error: {type(e).__name__}, reason: {str(e)}'\n", + " return inner" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Helper Code\n", + "To allow you to focus on implementing search instead of having to set up states, the class `Cube` provided in `cube.py` supports the following methods:\n", + "\n", + "- `goal_test(state)`: tests whether the provided `state` is the goal state.\n", + "\n", + "- `actions(state)`: returns a list of actions at the provided `state`.\n", + "\n", + "- `result(state, action)`: returns the new state after taking `action` from the provided `state`. It is deterministic.\n", + "\n", + "- `path_cost(c, state1, action, state2)`: returns the accumulated cost of reaching `state1` from the initial state and then reaching `state2` from `state1` by `action`.\n", + "\n", + "In the cube problem, the state of the cube is an instance of `State` class. It is a hashable type. `Action` in `Cube` is a tuple of an integer representing label and a string representing direction. Your search function should take and only take legal actions to transition from one state to another.\n", + "\n", + "For your convenience, we have provided a `Node` class for constructing a search tree and `PriorityQueue` class for your search algorithm in the `utils.py`. You may also choose to implement your own `Node` and `PriorityQueue` class instead. Our autograder will follow the same import structure as that of the `ps2.py`.\n", + "\n", + "Please run the following code block to use the helper classes. If you do not wish to use them, you may skip the execution of the following code block.\n", + "\n", + "**If you choose to override the provided helpers, please include all your code implementations in the template file `ps2.py` as well as Coursemology .**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + "We provide implementations for the Node and PriorityQueue classes in utils.py, but you can implement your own if you wish\n", + "\"\"\"\n", + "from utils import Node\n", + "from utils import PriorityQueue" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Task 1.1: Design a heuristic for A* Search\n", + "Implement the A* Search in two parts.\n", + "First, design your heuristic function `heuristic_func(problem, state)`, which takes in an instance of the `Cube` class and the `State` class (see below). It returns the estimated cost of reaching the goal state from the state given.\n", + "\n", + "**Note:**\n", + "1. The heuristic function estimates the “distance” to the goal state.\n", + "2. The heuristic should be *admissible* (never overestimates the cost to reach a goal) and *consistent* (obeys the triangle inequality). With an admissible and consistent heuristic, A* graph search is cost-optimal.\n", + "3. The template heuristic returns 0 for all the cases. It does not provide any information. Thus, you will see the connection between the A* search and the BFS graph search (PS1) in terms of performance.\n", + "4. Please try your best to find the best heuristic for this problem.\n", + "\n", + "**Hint:**\n", + "Think about how one action can affect multiple tiles instead of just one, i.e. how many tiles can be put into the right location per action maximally." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def heuristic_func(problem: cube.Cube, state: cube.State) -> float:\n", + " r\"\"\"\n", + " Computes the heuristic value of a state\n", + " \n", + " Args:\n", + " problem (cube.Cube): the problem to compute\n", + " state (cube.State): the state to be evaluated\n", + " \n", + " Returns:\n", + " h_n (float): the heuristic value \n", + " \"\"\"\n", + " h_n = 0.0\n", + " goals = problem.goal\n", + "\n", + " \"\"\" YOUR CODE HERE \"\"\"\n", + " \n", + " \"\"\" END YOUR CODE HERE \"\"\"\n", + "\n", + " return h_n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test case for Task 1.1\n", + "@wrap_test\n", + "def test_heuristic(case):\n", + "\n", + " input_dict = case['input_dict']\n", + " answer = case['answer']\n", + " problem = cube.Cube(input_dict = input_dict)\n", + "\n", + " assert heuristic_func(problem, problem.goal) == 0, \"Heuristic is not 0 at the goal state\"\n", + " assert heuristic_func(problem, problem.initial) <= answer['cost'], \"Heuristic is not admissible\"\n", + "\n", + " return \"PASSED\"\n", + "\n", + "cube1 = {'input_dict': {\"initial\": {'shape': [3, 3], 'layout': ['N', 'U', \n", + " 'S', 'N','U', 'S', 'N', 'U', 'S']}, 'goal': {'shape': [3, 3], 'layout': \n", + " ['N', 'U', 'S', 'N', 'U', 'S', 'N', 'U', 'S']}}, 'answer': {'solution': \n", + " [], 'cost': 0}}\n", + "\n", + "cube2 = {'input_dict': {\"initial\": {'shape': [3, 3], 'layout': ['S', 'O', \n", + " 'C', 'S', 'O', 'C', 'S', 'O', 'C']}, 'goal': {'shape': [3, 3], \n", + " 'layout': ['S', 'S', 'S', 'O', 'O', 'O', 'C', 'C', 'C']}}, 'answer': \n", + " {'solution': [[2, 'right'], [1, 'left'], [1, 'down'], \n", + " [2, 'up']], 'cost': 4}}\n", + "\n", + "cube3 = {'input_dict': {\"initial\": {'shape': [3, 3], 'layout': ['N', 'U', \n", + " 'S', 'N', 'U', 'S', 'N', 'U', 'S']}, 'goal': {'shape': [3, 3], 'layout': \n", + " ['S', 'U', 'N', 'N', 'S', 'U', 'U', 'N', 'S']}}, 'answer': {'solution': \n", + " [[0, 'left'], [1, 'right'], [0, 'up'], [1, 'down']], 'cost': 4}}\n", + "\n", + "cube4 = {'input_dict':{\"initial\": {'shape': [3, 4], 'layout': [1, 1, 9, 0,\n", + " 2, 2, 0, 2, 9, 0, 1, 9]}, 'goal': {'shape': [3, 4], 'layout': [ 1, 0,\n", + " 9, 2, 2, 1, 0, 9, 2, 1, 0, 9]}}, 'answer': {'solution': [[1, 'down'],\n", + " [3, 'up'], [2, 'left']], 'cost': 3}}\n", + "\n", + "print('cube1: ' + test_heuristic(cube1))\n", + "print('cube2: ' + test_heuristic(cube2))\n", + "print('cube3: ' + test_heuristic(cube3))\n", + "print('cube4: ' + test_heuristic(cube4))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Task 1.2: Implement A* search \n", + "\n", + "Implement an A* search function: `astar_search(problem)`, which takes in an instance of the `Cube` class, and returns a sequence of actions from the provided action set.\n", + "\n", + "**Note:**\n", + "\n", + "1. A* search is an extension of the best-first search algorithm that uses the evaluation function\n", + "\n", + " `f (state) = g(state) + h(state)`\n", + "\n", + " to estimate the cost of the optimal path from a state to a goal state.\n", + "\n", + "2. A* search should be aware of whether a new state has been reached.\n", + "3. A* search should explore the node with the lowest possible cost to the goal state in each step.\n", + "4. If a better path to an unexplored state is found, A* search should update its information in the “waiting list”.\n", + "\n", + "If there is no set of actions that can lead to the goal state, `astar_search(problem)` should return `False`. \n", + "\n", + "An implementation for `heuristic_func(problem, state)` has been provided on Coursemology for this section, in case you were unable to come up with a good heuristic. Locally, you should test A* using the heuristic you defined in Task 1.1. \n", + "\n", + "*Hint: It might be useful to create additional functions for the `PriorityQueue` class.*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def astar_search(problem: cube.Cube):\n", + " r\"\"\"\n", + " A* Search finds the solution to reach the goal from the initial.\n", + " If no solution is found, return False.\n", + " \n", + " Args:\n", + " problem (cube.Cube): Cube instance\n", + "\n", + " Returns:\n", + " solution (List[Action]): the action sequence\n", + " \"\"\"\n", + " fail = True\n", + " solution = []\n", + " \n", + " \"\"\" YOUR CODE HERE \"\"\"\n", + " \n", + " \"\"\" END YOUR CODE HERE \"\"\"\n", + " \n", + " if fail:\n", + " return False\n", + " return solution" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test cases for Tasks 1.1 and 1.2\n", + "@wrap_test\n", + "def test_astar(case):\n", + "\n", + " input_dict = case['input_dict']\n", + " answer = case['answer']\n", + " problem = cube.Cube(input_dict = input_dict)\n", + "\n", + " start = time.time()\n", + " solution = astar_search(problem)\n", + " print(f\"Time lapsed: {time.time() - start}\")\n", + "\n", + " if solution is False:\n", + " assert answer[\"solution\"] is False, \"Solution is not False\"\n", + " else:\n", + " correctness, cost = problem.verify_solution(solution, _print=False)\n", + " assert correctness, f\"Fail to reach goal state with solution {solution}\"\n", + " assert cost <= answer['cost'], f\"Cost is not optimal.\"\n", + " return \"PASSED\"\n", + "\n", + "print('cube1: ' + test_astar(cube1))\n", + "print('cube2: ' + test_astar(cube2))\n", + "print('cube3: ' + test_astar(cube3))\n", + "print('cube4: ' + test_astar(cube4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Task 1.3: Consistency & Admissibility\n", + "Explain why the heuristic you designed for Task 1.1 is *consistent* and *admissible*." + ] + }, + { + "attachments": { + "image-2.png": { + "image/png": "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" + } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The Travelling Salesman Problem (TSP)\n", + "\n", + "Your cousin Ben Bitdiddle is planning to start a company which sells premium imported chocolate from Europe. Among all cities in the country, Ben must choose one to be his company headquarters to receive shipment from Europe and devise a route from the headquarters to deliver the chocolate to every other city. This route must only visit each city **exactly once** and return to the headquarters to receive the next shipment. In addition, to save fuel cost, the route must be **as short as possible**. Given a list of cities and the distance between every two cities, what is the shortest possible route?\n", + "\n", + "This problem is a classic NP-hard optimisation problem in computer science. In this task, you will design and implement a local search algorithm to find a shortest route. You must find the route as **a list of cities** in the order of travel from the starting city to the last city before returning.\n", + "\n", + "For example, consider the graph below, which represents 4 cities and the distances between them.\n", + "\n", + "\n", + "\n", + "An optimal route is `[0, 1, 2, 3]`, with the minimal distance travelled of 1 + 2 + 2 + 3 = 8.\n", + "\n", + "**Note:**\n", + "* There can be more than 1 shortest route, e.g., `[1, 0, 3, 2]`, `[1, 3, 2, 0]`, etc. You only need to find one such route.\n", + "* `[0, 1, 2]` is not legal as the route must go through all 4 cities.\n", + "* `[0, 1, 2, 3, 1]` is not legal as city 1 is visited more than once.\n", + "* `[1, 3, 0, 2]` is legal but it is not the shortest route, as the distance travelled of 3 + 3 + 2 + 2 = 10." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Task 2.1: State representation\n", + "Propose a state representation for this problem if we want to formulate it as a local search problem." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Task 2.2: Initial and goal states\n", + "\n", + "What are the initial and goal states for the problem under your proposed representation?\n", + "\n", + "**Note:**\n", + "* In many optimization problems such as the TSP, the path to the goal is irrelevant; **the goal state itself is the solution to the problem**.\n", + "* Local search algorithms keep a single \"current\" state and move from a state to another in the search space by applying local changes (with the help of a *transition function*), until an optimal solution is found." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Can you do better?\n", + "\n", + "Recall that similar to A* search, local search utilises evaluation functions to decide how to transition from one state to another. However, being an uninformed guy, your cousin Ben Bitdiddle tells you to use the \"greedy\" solution. Given an incomplete route, the \"greedy\" solution builds a path by adding the closest unvisited node from the last visited node, until all nodes are visited. For instance, in the graph above, the \"greedy\" solution is `[0, 1, 2, 3]`.\n", + "\n", + "Although this solution seems relatively sensible, as a CS2109S student, you have a nagging feeling that it may not work all the time. Can you create an evaluation function and transition function to get better results with local search?\n", + "\n", + "\n", + "**Note:**\n", + "\n", + "* For the following tasks, we will be benchmarking your hill-climbing algorithm against our own version using the greedy solution. Note that the hidden test cases can be quite large, so any brute-force solution will not suffice. \n", + "\n", + "* Your own evaluation functions and transition functions may underperform against the greedy solution for small instances of TSP, but should outperform the greedy solution consistently for large instances. For our public and private test cases, we have designed the greedy solution to be suboptimal.\n", + "\n", + "* If your code does not pass the private test cases on Coursemology because it underperforms against the greedy solution, you may re-run your code a few times in case you are \"unlucky\" with random initial routes." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Task 2.3: State transitions\n", + "\n", + "Implement a reasonable transition function `transition(route)` to generate new routes by applying minor \"tweaks\" to the current route. It should return a list of new routes to be used in the next iteration in the hill-climbing algorithm.\n", + "\n", + "**Note:**\n", + "* At each iteration, the routes generated from the transition function are evaluated against each other (using an evaluation function). The best route will be selected for the next iteration if it is better than the current route.\n", + "* Your transition function should not return too many routes as it would take too much time for evaluation. (do not enumerate all possible states otherwise it will timeout, only generate \"neighbors\")\n", + "* However, if too few routes are generated, you are more likely to be stuck at a local maxima as each route will be compared against fewer routes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def transition(route: List[int]):\n", + " r\"\"\"\n", + " Generates new routes to be used in the next iteration in the hill-climbing algorithm.\n", + "\n", + " Args: \n", + " route (List[int]): The current route as a list of cities in the order of travel\n", + "\n", + " Returns:\n", + " new_routes (List[List[int]]): New routes to be considered\n", + " \"\"\"\n", + " new_routes = []\n", + " \n", + " \"\"\" YOUR CODE HERE \"\"\"\n", + " \n", + " \"\"\" END YOUR CODE HERE \"\"\"\n", + "\n", + " return new_routes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test case for Task 2.3\n", + "@wrap_test\n", + "def test_transition(route: List[int]):\n", + " for new_route in transition(route):\n", + " assert sorted(new_route) == list(range(len(route))), \"Invalid route\"\n", + "\n", + " return \"PASSED\"\n", + "\n", + "print(test_transition([1, 3, 2, 0]))\n", + "print(test_transition([7, 8, 6, 3, 5, 4, 9, 2, 0, 1]))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Task 2.4: Evaluation function\n", + "Implement an evaluation function `evaluation_func(cities, distances, route)` that would be helpful in deciding on the \"goodness\" of a route, i.e. an optimal route should return a higher evaluation score than a suboptimal one." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def evaluation_func(cities: int, distances: List[Tuple[int]], route: List[int]) -> float:\n", + " r\"\"\"\n", + " Computes the evaluation score of a route\n", + "\n", + " Args:\n", + " cities (int): The number of cities to be visited\n", + "\n", + " distances (List[Tuple[int]]): The list of distances between every two cities\n", + " Each distance is represented as a tuple in the form of (c1, c2, d), where\n", + " c1 and c2 are the two cities and d is the distance between them.\n", + " The length of the list should be equal to cities * (cities - 1)/2.\n", + "\n", + " route (List[int]): The current route as a list of cities in the order of travel\n", + "\n", + " Returns:\n", + " h_n (float): the evaluation score\n", + " \"\"\"\n", + " h_n = 0.0\n", + " \n", + " \"\"\" YOUR CODE HERE \"\"\"\n", + " \n", + " \"\"\" END YOUR CODE HERE \"\"\"\n", + "\n", + " return h_n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test case for Task 2.4\n", + "cities = 4\n", + "distances = [(1, 0, 10), (0, 3, 22), (2, 1, 8), (2, 3, 30), (1, 3, 25), (0, 2, 15)]\n", + "\n", + "route_1 = evaluation_func(cities, distances, [0, 1, 2, 3])\n", + "route_2 = evaluation_func(cities, distances, [2, 1, 3, 0])\n", + "route_3 = evaluation_func(cities, distances, [1, 3, 2, 0])\n", + "\n", + "print(route_1 == route_2) # True\n", + "print(route_1 > route_3) # True" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Task 2.5: Explain your evaluation function\n", + "\n", + "Explain why your evaluation function is suitable for this problem. " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Task 2.6: Implement hill-climbing\n", + "Using your representation above, implement the hill-climbing algorithm `hill_climbing(cities, distances)`, which takes in the number of cities and the list of distances, and returns the shortest route as a list of cities.\n", + "\n", + "1. The hill-climbing approach is a local search algorithm which starts with a randomly-initialised state and continuously selects the next candidate solution that locally maximizes the reduction of the evaluation function.\n", + "\n", + "2. The algorithm terminates when a (local) maxima is reached, i.e. a solution that cannot be improved further by looking at the next candidate solutions.\n", + "\n", + "3. Unlike previous search algorithms you have implemented, hill-climbing only keeps a single current state. As such, it does not involve a search tree/graph. Backtracking is also not possible.\n", + "\n", + "An implementation for `evaluation_func(cities, distances, route)` has been provided on Coursemology for this section, in case you were unable to come up with a good evaluation function and transition function. Locally, you can test your hill-climbing implementation using the functions you defined in Task 2.3." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def hill_climbing(cities: int, distances: List[Tuple[int]]):\n", + " r\"\"\"\n", + " Hill climbing finds the solution to reach the goal from the initial.\n", + "\n", + " Args:\n", + " cities (int): The number of cities to be visited\n", + "\n", + " distances (List[Tuple[int]]): The list of distances between every two cities\n", + " Each distance is represented as a tuple in the form of (c1, c2, d), where\n", + " c1 and c2 are the two cities and d is the distance between them.\n", + " The length of the list should be equal to cities * (cities - 1)/2.\n", + "\n", + " Returns:\n", + " route (List[int]): The shortest route, represented by a list of cities\n", + " in the order to be traversed.\n", + " \"\"\"\n", + "\n", + " route = []\n", + "\n", + " \"\"\" YOUR CODE HERE \"\"\"\n", + "\n", + " \"\"\" END YOUR CODE HERE \"\"\"\n", + "\n", + " return route" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test cases for Task 2.3, 2.5, 2.6\n", + "@wrap_test\n", + "def test_hill_climbing(cities: int, distances: List[Tuple[int]]):\n", + " start = time.time()\n", + " route = hill_climbing(cities, distances)\n", + " print(f\"Time lapsed: {time.time() - start}\")\n", + "\n", + " assert sorted(route) == list(range(cities)), \"Invalid route\"\n", + "\n", + " return \"PASSED\"\n", + "\n", + "cities_1 = 4\n", + "distances_1 = [(1, 0, 10), (0, 3, 22), (2, 1, 8), (2, 3, 30), (1, 3, 25), (0, 2, 15)]\n", + "\n", + "cities_2 = 10\n", + "distances_2 = [(2, 7, 60), (1, 6, 20), (5, 4, 70), (9, 8, 90), (3, 7, 54), (2, 5, 61),\n", + " (4, 1, 106), (0, 6, 51), (3, 1, 45), (0, 5, 86), (9, 2, 73), (8, 4, 14), (0, 1, 51),\n", + " (9, 7, 22), (3, 2, 22), (8, 1, 120), (5, 7, 92), (5, 6, 60), (6, 2, 10), (8, 3, 78),\n", + " (9, 6, 82), (0, 2, 41), (2, 8, 99), (7, 8, 71), (0, 9, 32), (4, 0, 73), (0, 3, 42),\n", + " (9, 1, 80), (4, 2, 85), (5, 9, 113), (3, 6, 28), (5, 8, 81), (3, 9, 72), (9, 4, 81),\n", + " (5, 3, 45), (7, 4, 60), (6, 8, 106), (0, 8, 85), (4, 6, 92), (7, 6, 70), (7, 0, 22),\n", + " (7, 1, 73), (4, 3, 64), (5, 1, 80), (2, 1, 22)]\n", + "\n", + "print('cities_1: ' + test_hill_climbing(cities_1, distances_1))\n", + "print('cities_2: ' + test_hill_climbing(cities_2, distances_2))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Task 2.7: Improve hill-climbing with random restarts\n", + "\n", + "When no \"better\" neighbouring solutions are present, local search can be stuck at a local maxima. One way to combat this is to simply repeat local search from random initial states, taking the best performing iteration. \n", + "\n", + "Implement `hill_climbing_with_random_restarts(cities, distances, repeats)` by repeating hill climbing at different random locations.\n", + "\n", + "* Implementations for `evaluation_func(cities, distances, route)` and `hill_climbing(cities, distances)` has been provided on Coursemology for this section, but you can redefine it with your own version if you wish.\n", + "* Note that the implemented `evaluation_func(cities, distances, route)` returns a float, which can be from `float(-inf)` to `float(inf)`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def hill_climbing_with_random_restarts(cities: int, distances: List[Tuple[int]], repeats: int = 10):\n", + " r\"\"\"\n", + " Hill climbing with random restarts finds the solution to reach the goal from the initial.\n", + "\n", + " Args:\n", + " cities (int): The number of cities to be visited\n", + "\n", + " distances (List[Tuple[int]]): The list of distances between every two cities\n", + " Each distance is represented as a tuple in the form of (c1, c2, d), where\n", + " c1 and c2 are the two cities and d is the distance between them.\n", + " The length of the list should be equal to cities * (cities - 1)/2.\n", + "\n", + " repeats (int): The number of times hill climbing to be repeated. The default\n", + " value is 10.\n", + "\n", + " Returns:\n", + " route (List[int]): The shortest route, represented by a list of cities\n", + " in the order to be traversed.\n", + " \"\"\"\n", + "\n", + " route = []\n", + "\n", + " \"\"\" YOUR CODE HERE \"\"\"\n", + "\n", + " \"\"\" END YOUR CODE HERE \"\"\"\n", + "\n", + " return route" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test cases for Task 2.7\n", + "@wrap_test\n", + "def test_random_restarts(cities: int, distances: List[Tuple[int]], repeats: int = 10):\n", + " start = time.time()\n", + " route = hill_climbing_with_random_restarts(cities, distances, repeats)\n", + " print(f\"Time lapsed: {time.time() - start}\")\n", + "\n", + " assert sorted(route) == list(range(cities)), \"Invalid route\"\n", + "\n", + " return \"PASSED\"\n", + "\n", + "cities_1 = 4\n", + "distances_1 = [(1, 0, 10), (0, 3, 22), (2, 1, 8), (2, 3, 30), (1, 3, 25), (0, 2, 15)]\n", + "\n", + "cities_2 = 10\n", + "distances_2 = [(2, 7, 60), (1, 6, 20), (5, 4, 70), (9, 8, 90), (3, 7, 54), (2, 5, 61),\n", + " (4, 1, 106), (0, 6, 51), (3, 1, 45), (0, 5, 86), (9, 2, 73), (8, 4, 14), (0, 1, 51),\n", + " (9, 7, 22), (3, 2, 22), (8, 1, 120), (5, 7, 92), (5, 6, 60), (6, 2, 10), (8, 3, 78),\n", + " (9, 6, 82), (0, 2, 41), (2, 8, 99), (7, 8, 71), (0, 9, 32), (4, 0, 73), (0, 3, 42),\n", + " (9, 1, 80), (4, 2, 85), (5, 9, 113), (3, 6, 28), (5, 8, 81), (3, 9, 72), (9, 4, 81),\n", + " (5, 3, 45), (7, 4, 60), (6, 8, 106), (0, 8, 85), (4, 6, 92), (7, 6, 70), (7, 0, 22),\n", + " (7, 1, 73), (4, 3, 64), (5, 1, 80), (2, 1, 22)]\n", + "\n", + "print('cities_1: ' + test_random_restarts(cities_1, distances_1))\n", + "print('cities_2: ' + test_random_restarts(cities_2, distances_2, 20))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Task 2.8: Comparison between local search and other search algorithms\n", + "\n", + "Compared to previous search algorithms you have seen (uninformed search, A*), why do you think local search is more suitable for this problem?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test Cases\n", + "\n", + "To help with your implementation, we have provided some examples as test cases. These are not sufficient to ensure that your code is works correctly, and we encourage you to write your own additional test cases to test and debug your code.\n", + "\n", + "Note that your answers may be slightly different from the answers provided since multiple valid solutions sharing the same cost may exist. During grading, your code will be evaluated on hidden test cases on top of the ones we have provided. We will validate your solution and compare the resulting cost to the expected optimal cost.\n", + "\n", + "Also note that we will have hidden test case(s) to check the quality of your heuristic functions in the A* search and local search algorithms. Basically, a good heuristic function should provide valuable information to the search, and thus it reduces the number of explorations before finding the best solution. You can keep track of the size of the “reached” state to help you design a better heuristic.\n", + "\n", + "