From 01ffa3b6aa2e50b37c729b62f85a785f67ece0be Mon Sep 17 00:00:00 2001 From: Yadunand Prem Date: Thu, 8 Feb 2024 00:40:04 +0800 Subject: [PATCH] feat: 2109s ps1 --- cs2109s/labs/ps2/cube.py | 295 +++++++++++ cs2109s/labs/ps2/graph.png | Bin 0 -> 44946 bytes cs2109s/labs/ps2/ps2.ipynb | 855 ++++++++++++++++++++++++++++++ cs2109s/labs/ps2/ps2.py | 442 +++++++++++++++ cs2109s/labs/ps2/utils.py | 94 ++++ ma1522/assignment/assignment1.pdf | Bin 32999 -> 33000 bytes 6 files changed, 1686 insertions(+) create mode 100644 cs2109s/labs/ps2/cube.py create mode 100644 cs2109s/labs/ps2/graph.png create mode 100644 cs2109s/labs/ps2/ps2.ipynb create mode 100644 cs2109s/labs/ps2/ps2.py create mode 100644 cs2109s/labs/ps2/utils.py 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 0000000000000000000000000000000000000000..670b26f51b1b6d125b0f4979086920eb8d52100d GIT binary patch literal 44946 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 RO`5BI#_37BqW^i*{{YG53Ag|N literal 0 HcmV?d00001 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", + "![image.png](attachment:image-2.png)\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", + "
Have fun and enjoy coding.
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Submission\n", + "\n", + "Once you are done, please submit your work to Coursemology, by copying the right snippets of code into the corresponding box that says 'Your answer', and click 'Save'. After you save, you can make changes to your\n", + "submission.\n", + "\n", + "Once you are satisfied with what you have uploaded, click 'Finalize submission.' **Note that once your submission is finalized, it is considered to be submitted for grading and cannot be changed**. If you need to undo\n", + "this action, you will have to email your assigned tutor for help. Please do not finalize your submission until you are sure that you want to submit your solutions for grading. \n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.9" + }, + "vscode": { + "interpreter": { + "hash": "369f2c481f4da34e4445cda3fffd2e751bd1c4d706f27375911949ba6bb62e1c" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/cs2109s/labs/ps2/ps2.py b/cs2109s/labs/ps2/ps2.py new file mode 100644 index 0000000..b8b3162 --- /dev/null +++ b/cs2109s/labs/ps2/ps2.py @@ -0,0 +1,442 @@ +""" CS2109S Problem Set 2: Informed Search""" + +import copy +import heapq +import math +import random +import time +from typing import List, Tuple + +import cube +import utils + +""" ADD HELPER FUNCTION HERE """ + +""" +We provide implementations for the Node and PriorityQueue classes in utils.py, but you can implement your own if you wish +""" +from utils import Node +from utils import PriorityQueue + + +#TODO Task 1.1: Implement your heuristic function, which takes in an instance of the Cube and +# the State class and returns the estimated cost of reaching the goal state from the state given. +def heuristic_func(problem: cube.Cube, curr_state: cube.State) -> float: + r""" + Computes the heuristic value of a state + + Args: + problem (cube.Cube): the problem to compute + state (cube.State): the state to be evaluated + + Returns: + h_n (float): the heuristic value + """ + h_n = 0.0 + goals = problem.goal + """ YOUR CODE HERE """ + for idx, val in enumerate(curr_state.layout): + if val != goals.layout[idx]: + h_n += 1 + """ END YOUR CODE HERE """ + + h_n /= max(curr_state.shape) + + return h_n + +# Test +def wrap_test(func): + def inner(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + return f'FAILED, error: {type(e).__name__}, reason: {str(e)}' + return inner + +# Test case for Task 1.1 +@wrap_test +def test_heuristic(case): + + input_dict = case['input_dict'] + answer = case['answer'] + problem = cube.Cube(input_dict = input_dict) + + assert heuristic_func(problem, problem.goal) == 0, "Heuristic is not 0 at the goal state" + assert heuristic_func(problem, problem.initial) <= answer['cost'], "Heuristic is not admissible" + + return "PASSED" + +if __name__ == '__main__': + + cube1 = {'input_dict': {"initial": {'shape': [3, 3], 'layout': ['N', 'U', + 'S', 'N','U', 'S', 'N', 'U', 'S']}, 'goal': {'shape': [3, 3], 'layout': + ['N', 'U', 'S', 'N', 'U', 'S', 'N', 'U', 'S']}}, 'answer': {'solution': + [], 'cost': 0}} + + cube2 = {'input_dict': {"initial": {'shape': [3, 3], 'layout': ['S', 'O', + 'C', 'S', 'O', 'C', 'S', 'O', 'C']}, 'goal': {'shape': [3, 3], + 'layout': ['S', 'S', 'S', 'O', 'O', 'O', 'C', 'C', 'C']}}, 'answer': + {'solution': [[2, 'right'], [1, 'left'], [1, 'down'], + [2, 'up']], 'cost': 4}} + + cube3 = {'input_dict': {"initial": {'shape': [3, 3], 'layout': ['N', 'U', + 'S', 'N', 'U', 'S', 'N', 'U', 'S']}, 'goal': {'shape': [3, 3], 'layout': + ['S', 'U', 'N', 'N', 'S', 'U', 'U', 'N', 'S']}}, 'answer': {'solution': + [[0, 'left'], [1, 'right'], [0, 'up'], [1, 'down']], 'cost': 4}} + + cube4 = {'input_dict':{"initial": {'shape': [3, 4], 'layout': [1, 1, 9, 0, + 2, 2, 0, 2, 9, 0, 1, 9]}, 'goal': {'shape': [3, 4], 'layout': [ 1, 0, + 9, 2, 2, 1, 0, 9, 2, 1, 0, 9]}}, 'answer': {'solution': [[1, 'down'], + [3, 'up'], [2, 'left']], 'cost': 3}} + + print('Task 1.1:') + print('cube1: ' + test_heuristic(cube1)) + print('cube2: ' + test_heuristic(cube2)) + print('cube3: ' + test_heuristic(cube3)) + print('cube4: ' + test_heuristic(cube4)) + print('\n') + + +#TODO Task 1.2: Implement A* search which takes in an instance of the Cube +# class and returns a list of actions [(2,'left'), ...] from the provided action set. +def astar_search(problem: cube.Cube): + r""" + A* Search finds the solution to reach the goal from the initial. + If no solution is found, return False. + + Args: + problem (cube.Cube): Cube instance + + Returns: + solution (List[Action]): the action sequence + """ + fail = True + solution = [] + + frontier = PriorityQueue() + visited = set() + + + """ YOUR CODE HERE """ + initial_state = Node(None, (), problem.initial, 0, heuristic_func(problem, problem.initial)) + frontier.push(initial_state.get_fn(), initial_state) + + while frontier: + curr_node: Node = frontier.pop() + if problem.goal_test(curr_node.state): + fail = False + solution = list(curr_node.act) + break + if curr_node in visited: + continue + visited.add(curr_node) + for action in problem.actions(curr_node.state): + next_state = problem.result(curr_node.state, action) + next_node = Node(curr_node, curr_node.act + (action,), next_state, problem.path_cost(curr_node.g_n, curr_node.state, action, next_state), heuristic_func(problem, next_state)) + frontier.push(next_node.get_fn(), next_node) + + """ END YOUR CODE HERE """ + + if fail: + return False + return solution + +@wrap_test +def test_astar(case): + + input_dict = case['input_dict'] + answer = case['answer'] + problem = cube.Cube(input_dict = input_dict) + + start = time.time() + solution = astar_search(problem) + print(f"Time lapsed: {time.time() - start}") + + if solution is False: + assert answer['solution'] is False, "Solution is not False" + else: + correctness, cost = problem.verify_solution(solution, _print=False) + assert correctness, f"Fail to reach goal state with solution {solution}" + assert cost <= answer['cost'], f"Cost is not optimal." + return "PASSED" + + +if __name__ == '__main__': + + cube1 = {'input_dict': {"initial": {'shape': [3, 3], 'layout': ['N', 'U', + 'S', 'N','U', 'S', 'N', 'U', 'S']}, 'goal': {'shape': [3, 3], 'layout': + ['N', 'U', 'S', 'N', 'U', 'S', 'N', 'U', 'S']}}, 'answer': {'solution': + [], 'cost': 0}} + + cube2 = {'input_dict': {"initial": {'shape': [3, 3], 'layout': ['S', 'O', + 'C', 'S', 'O', 'C', 'S', 'O', 'C']}, 'goal': {'shape': [3, 3], + 'layout': ['S', 'S', 'S', 'O', 'O', 'O', 'C', 'C', 'C']}}, 'answer': + {'solution': [[2, 'right'], [1, 'left'], [1, 'down'], + [2, 'up']], 'cost': 4}} + + cube3 = {'input_dict': {"initial": {'shape': [3, 3], 'layout': ['N', 'U', + 'S', 'N', 'U', 'S', 'N', 'U', 'S']}, 'goal': {'shape': [3, 3], 'layout': + ['S', 'U', 'N', 'N', 'S', 'U', 'U', 'N', 'S']}}, 'answer': {'solution': + [[0, 'left'], [1, 'right'], [0, 'up'], [1, 'down']], 'cost': 4}} + + cube4 = {'input_dict':{"initial": {'shape': [3, 4], 'layout': [1, 1, 9, 0, + 2, 2, 0, 2, 9, 0, 1, 9]}, 'goal': {'shape': [3, 4], 'layout': [ 1, 0, + 9, 2, 2, 1, 0, 9, 2, 1, 0, 9]}}, 'answer': {'solution': [[1, 'down'], + [3, 'up'], [2, 'left']], 'cost': 3}} + + print('Task 1.2:') + print('cube1: ' + test_astar(cube1)) + print('cube2: ' + test_astar(cube2)) + print('cube3: ' + test_astar(cube3)) + print('cube4: ' + test_astar(cube4)) + print('\n') + + + +#TODO Task 1.3: Explain why the heuristic you designed for Task 1.1 is {consistent} +# and {admissible}. +#A heuristic is admissible if for every node, h(n) <= h*(n), where h*(n) is the true cost (in this case, the number of operations, left / right / up / down rotations on a column). + +# In this case, my heuristic calculates the mismatches in position against the goal divided by the max(row, col).  +# For a 3x3 grid, assuming the worst case initial state, where everything is not in its correct position, where there is 9 mismatches. Then h(initial) = 9/3 = 3. And at this state, it would take a minimum of 3 moves to get to its original state. Thus, h(initial) <= h*(initial). At goal state, h(goal) = 0/3 = 0 <= h*(goal). + + + + +#TODO Task 2.1: Propose a state representation for this problem if we want to formulate it +# as a local search problem. +# State representation: List[int] The current route as a list of cities in the order of travel + + +#TODO Task 2.2: What are the initial and goal states under your proposed representation? +# Initial state: (List[int]) The current route as a list of cities in the order of travel +# Goal state: (List[int]) The shortest route, represented by a list of cities in the order to be traversed. + + +#TODO Task 2.3: Implement a reasonable transition function 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. +def transition(route: List[int]): + r""" + Generates new routes to be used in the next iteration in the hill-climbing algorithm. + + Args: + route (List[int]): The current route as a list of cities in the order of travel + + Returns: + new_routes (List[List[int]]): New routes to be considered + """ + new_routes = [] + + """ YOUR CODE HERE """ + import copy + for i in range(len(route)): + for j in range(i+1, len(route)): + new_route = copy.deepcopy(route) + new_route[i], new_route[j] = new_route[j], new_route[i] + new_routes.append(new_route) + """ END YOUR CODE HERE """ + + return new_routes + +# Test +@wrap_test +def test_transition(route: List[int]): + counter = 0 + for new_route in transition(route): + counter += 1 + assert sorted(new_route) == list(range(len(route))), "Invalid route" + print(f"Number of new routes: {counter}, with route length: {len(route)}") + + return "PASSED" + +if __name__ == '__main__': + print('Task 2.3:') + print(test_transition([1, 3, 2, 0])) + print(test_transition([7, 8, 6, 3, 5, 4, 9, 2, 0, 1])) + print('\n') + + +#TODO Task 2.4: 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. +def evaluation_func(cities: int, distances: List[Tuple[int]], route: List[int]) -> float: + r""" + Computes the evaluation score of a route + + Args: + cities (int): The number of cities to be visited + + distances (List[Tuple[int]]): The list of distances between every two cities + Each distance is represented as a tuple in the form of (c1, c2, d), where + c1 and c2 are the two cities and d is the distance between them. + The length of the list should be equal to cities * (cities - 1)/2. + + route (List[int]): The current route as a list of cities in the order of travel + + Returns: + h_n (float): the evaluation score + """ + h_n = 0.0 + + + """ YOUR CODE HERE """ + # distance from the first city to the last city + dist = {} + for d in distances: + dist[(d[0], d[1])] = d[2] + dist[(d[1], d[0])] = d[2] + for i in range(len(route)): + c1, c2 = route[i-1], route[i] + h_n += dist[(c1, c2)] + # last city to the first city + h_n = 1/h_n + """ END YOUR CODE HERE """ + return h_n + +if __name__ == '__main__': + cities = 4 + distances = [(1, 0, 10), (0, 3, 22), (2, 1, 8), (2, 3, 30), (1, 3, 25), (0, 2, 15)] + + route_1 = evaluation_func(cities, distances, [0, 1, 2, 3]) + route_2 = evaluation_func(cities, distances, [2, 1, 3, 0]) + route_3 = evaluation_func(cities, distances, [1, 3, 2, 0]) + print(f"route_1: {route_1}, route_2: {route_2}, route_3: {route_3}") + + print('Task 2.4:') + print(route_1 == route_2) # True + print(route_1 > route_3) # True + print('\n') + + +#TODO Task 2.5: Explain why your evaluation function is suitable for this problem. +# The evaluation function calculates the inverse of total distance travelled. Since we're trying to minimise the +# distance, we're trying to maximise the inverse of the distance. Thus, the evaluation function is suitable. + + +#TODO Task 2.6: Implement hill-climbing which takes in the number of cities and the list of +# distances, and returns the shortest route as a list of cities. +def hill_climbing(cities: int, distances: List[Tuple[int]]): + r""" + Hill climbing finds the solution to reach the goal from the initial. + + Args: + cities (int): The number of cities to be visited + + distances (List[Tuple[int]]): The list of distances between every two cities + Each distance is represented as a tuple in the form of (c1, c2, d), where + c1 and c2 are the two cities and d is the distance between them. + The length of the list should be equal to cities * (cities - 1)/2. + + Returns: + route (List[int]): The shortest route, represented by a list of cities + in the order to be traversed. + """ + route = [] + + """ YOUR CODE HERE """ + # I'm going to make an assumption that the graph is connected so that I don't have to find a path between every pair of cities. + # https://chat.openai.com/share/588631bb-6261-48cc-8490-e29401febad9 to explain hill climbing and hwo to randomize shuffle + initial_state = list(range(cities)) + random.shuffle(initial_state) + curr_h_n = evaluation_func(cities, distances, initial_state) + while True: + states = transition(initial_state) + eval_states = [(evaluation_func(cities, distances, state), state) for state in states] + max_h_n, max_state = max(eval_states) + if curr_h_n >= max_h_n: + return initial_state + initial_state = max_state + curr_h_n = max_h_n + """ END YOUR CODE HERE """ + + return route + +# Test +@wrap_test +def test_hill_climbing(cities: int, distances: List[Tuple[int]]): + start = time.time() + route = hill_climbing(cities, distances) + print(f"Time lapsed: {(time.time() - start)*1000}") + + assert sorted(route) == list(range(cities)), "Invalid route" + + return "PASSED" + +if __name__ == '__main__': + + cities_1 = 4 + distances_1 = [(1, 0, 10), (0, 3, 22), (2, 1, 8), (2, 3, 30), (1, 3, 25), (0, 2, 15)] + + cities_2 = 10 + distances_2 = [(2, 7, 60), (1, 6, 20), (5, 4, 70), (9, 8, 90), (3, 7, 54), (2, 5, 61), (4, 1, 106), (0, 6, 51), (3, 1, 45), (0, 5, 86), (9, 2, 73), (8, 4, 14), (0, 1, 51), (9, 7, 22), (3, 2, 22), (8, 1, 120), (5, 7, 92), (5, 6, 60), (6, 2, 10), (8, 3, 78), (9, 6, 82), (0, 2, 41), (2, 8, 99), (7, 8, 71), (0, 9, 32), (4, 0, 73), (0, 3, 42), (9, 1, 80), (4, 2, 85), (5, 9, 113), (3, 6, 28), (5, 8, 81), (3, 9, 72), (9, 4, 81), (5, 3, 45), (7, 4, 60), (6, 8, 106), (0, 8, 85), (4, 6, 92), (7, 6, 70), (7, 0, 22), (7, 1, 73), (4, 3, 64), (5, 1, 80), (2, 1, 22)] + print('Task 2.6:') + print('cities_1: ' + test_hill_climbing(cities_1, distances_1)) + print('cities_2: ' + test_hill_climbing(cities_2, distances_2)) + print('\n') + + +#TODO Task 2.7: Implement hill_climbing_with_random_restarts by repeating hill climbing +# at different random locations. +def hill_climbing_with_random_restarts(cities: int, distances: List[Tuple[int]], repeats: int = 10): + r""" + Hill climbing with random restarts finds the solution to reach the goal from the initial. + + Args: + cities (int): The number of cities to be visited + + distances (List[Tuple[int]]): The list of distances between every two cities + Each distance is represented as a tuple in the form of (c1, c2, d), where + c1 and c2 are the two cities and d is the distance between them. + The length of the list should be equal to cities * (cities - 1)/2. + + repeats (int): The number of times hill climbing to be repeated. The default + value is 10. + + Returns: + route (List[int]): The shortest route, represented by a list of cities + in the order to be traversed. + """ + + route = [] + + """ YOUR CODE HERE """ + results = [hill_climbing(cities, distances) for _ in range(repeats)] + results = sorted(results, key=lambda x: evaluation_func(cities, distances, x)) + route = results[-1] + """ END YOUR CODE HERE """ + + return route + +# Test +@wrap_test +def test_random_restarts(cities: int, distances: List[Tuple[int]], repeats: int = 10): + start = time.time() + route = hill_climbing_with_random_restarts(cities, distances, repeats) + print(f"Time lapsed: {time.time() - start}") + + assert sorted(route) == list(range(cities)), "Invalid route" + + return "PASSED" + +if __name__ == '__main__': + + cities_1 = 4 + distances_1 = [(1, 0, 10), (0, 3, 22), (2, 1, 8), (2, 3, 30), (1, 3, 25), (0, 2, 15)] + + cities_2 = 10 + distances_2 = [(2, 7, 60), (1, 6, 20), (5, 4, 70), (9, 8, 90), (3, 7, 54), (2, 5, 61), + (4, 1, 106), (0, 6, 51), (3, 1, 45), (0, 5, 86), (9, 2, 73), (8, 4, 14), (0, 1, 51), + (9, 7, 22), (3, 2, 22), (8, 1, 120), (5, 7, 92), (5, 6, 60), (6, 2, 10), (8, 3, 78), + (9, 6, 82), (0, 2, 41), (2, 8, 99), (7, 8, 71), (0, 9, 32), (4, 0, 73), (0, 3, 42), + (9, 1, 80), (4, 2, 85), (5, 9, 113), (3, 6, 28), (5, 8, 81), (3, 9, 72), (9, 4, 81), + (5, 3, 45), (7, 4, 60), (6, 8, 106), (0, 8, 85), (4, 6, 92), (7, 6, 70), (7, 0, 22), + (7, 1, 73), (4, 3, 64), (5, 1, 80), (2, 1, 22)] + + print('Task 2.7:') + print('cities_1: ' + test_random_restarts(cities_1, distances_1)) + print('cities_2: ' + test_random_restarts(cities_2, distances_2, 20)) + + +#TODO Task 2.8: Compared to previous search algorithms you have seen (uninformed search, A*), +# why do you think local search is more suitable for this problem? diff --git a/cs2109s/labs/ps2/utils.py b/cs2109s/labs/ps2/utils.py new file mode 100644 index 0000000..9609004 --- /dev/null +++ b/cs2109s/labs/ps2/utils.py @@ -0,0 +1,94 @@ +import heapq + +class Node: + r"""Node class for search tree + Args: + parent (Node): the parent node of this node in the tree + act (Action): the action taken from parent to reach this node + state (State): the state of this node + g_n (float): the path cost of reaching this state + h_n (float): the heuristic value of this state + """ + + def __init__( + self, + parent: "Node", + act, + state, + g_n: float = 0.0, + h_n: float = 0.0): + + self.parent = parent # where am I from + self.act = act # how to get here + self.state = state # who am I + self.g_n = g_n # what it costs to be here g(n) + self.h_n = h_n # heuristic function h(n) + + def get_fn(self): + r""" + Returns the sum of heuristic and cost of the node + """ + return self.g_n + self.h_n + + def __str__(self): + return str(self.state) + + def __lt__(self, node): + """Compare the path cost between states""" + return self.g_n < node.g_n + + def __eq__(self, node): + """Compare whether two nodes have the same state""" + return isinstance(node, Node) and self.state == node.state + + def __hash__(self): + """Node can be used as a KeyValue""" + return hash(self.state) + + +class PriorityQueue: + def __init__(self): + self.heap = [] + + def __contains__(self, node): + """Decide whether the node (state) is in the queue""" + return any([item == node for _, item in self.heap]) + + def __delitem__(self, node): + """Delete the an existing node in the queue""" + try: + del self.heap[[item == node for _, item in self.heap].index(True)] + except ValueError: + raise KeyError(str(node) + " is not in the queue") + heapq.heapify(self.heap) # O(n) + + def __getitem__(self, node): + """Return the priority of the given node in the queue""" + for value, item in self.heap: + if item == node: + return value + raise KeyError(str(node) + " is not in the queue") + + def __len__(self): + return len(self.heap) + + def __repr__(self): + string = '[' + for priority, node in self.heap: + string += f"({priority}, {node}), " + string += ']' + return string + + def push(self, priority, node): + """Enqueue node with priority""" + heapq.heappush(self.heap, (priority, node)) + + def pop(self): + """Dequeue node with highest priority (the minimum one)""" + if self.heap: + return heapq.heappop(self.heap)[1] + else: + raise Exception("Empty priority queue") + + def get_priority(self, node): + return self.__getitem__(node) diff --git a/ma1522/assignment/assignment1.pdf b/ma1522/assignment/assignment1.pdf index 126969f8a120d4822d80b74b4aefe7045a7d1391..1bfdc218845f78b950d4108c812370b5ea31d6d9 100644 GIT binary patch delta 1627 zcmV-h2Bi7tfdc4(0;=EL(cz=-&U#P^)w$JM_;M7O6S9(6D@f) z>F3$odH(vo7!AiTEN*4#qJhNf(uL2E?KMY{jni(?9Zha&>xqLe9ex?#@u_91hqi8> zt~qEhopTF0M8V=obLj{2XpgXGt55ukI&b+-G*$#irFwt%gXn#LIF!<@jE+Zt0iT*} z!;c;^E6864T)`H~(;k+4MSImXB3$(8?H?DXo9!PHqjdj|aU$Yqdu)h}b{%EfR+^4h zZH#|Mo3;|Fqg^aCylwDiP>6Gov5KCK32jQOYU zRNCp3?ih3$I05s)p^x~=s7Oc=t4P*I2Ed0gmiT~vCY`5;2#KZ8?nOjqblYMkSKM}t zi!F&4Bvz3qQ#jNsx(!5YCkRlPHEplJQQ05SQbu0K>$1eRNi3j3A~C>!PnUhN6F?Uk z$93CDiGU7u&_+_IZ6pP)h!n^m_2s+d(U>SJOZz&D*_Q57vg`ZLMVxFd&gh*gcy`jA z=dt$U#qq_bcfY-Sbv3!`j)gotnT+}*d;NW0l+(@}m$%K)bZ6`kXB)|(Bdn4jqyl4iZL_gH#(_MWaSo0YtV z+Q?_e^B(o&xur8+%pZ!Nvuj{_M|X*sJEt%5;q8}F==kgH`(YomzZE|AbwaDgp>~!X zN>3NS2u}p8>tq+0bBDd-@$D#QJUbtCZ|8KsD-1{P-?687f|+`MJelWDc>Uw~($CY% zKAP>f5J=VM!%?nSC^yx_XkM^jH`u)xqJ75Zu%`U*MfPmmoAfaZSyEQg)$c<8gw|kN zuB11Ux8otCb7s{p2%Ej+{b_lF*{sjD|?BT_|!`T=MQj`ZQVcAQ~a8z=!bjC#fS6b_iqir zGrqib2EK3wzA}oXS7^5w&c+3Bs)7etasi{Um6n}kcx)BQfR5og$fk5Q8s&qcn3nk& zA*f%d;&YKy)Gt(#e@iOzZ>`2nmU!5k^N@1x_$uc-;aoJFe|+}#?dwzMy4pm!-IsFf zRZY4@&@DxO;3nn~kL%29ioN&+G7s25^OSfewh*6?m`z9b^Xz~5Y?4jJc~<;A$p$#o zW`p6^Jj?&O?~W3ZQ^WV=*vTc1)egTly5acv^2O<=OHyUlDRz%VvEM|os3}&MSXL?4 z%M>pWS$vu0c}eGdm^y1>+}<>rB^vDtjb1i1hGcqwcz5>h`OOtHy}EGgJotAtcm5?IbIHVg z+4LfRWz)+Y)1|Quoo8nk&)&Z$Z(nyo!5=G>-KJ2MuJ%6cZ&$p}tDl-JlOT_CCL%qX z=U?-27I7z>Cp_%UxL#u1Z1-Q45Pp97=J)GA;^1yzTs)flhK=!V;J);qMiPIVU)mXX zyVZR^Pm0uEQUxvAyKZ($%T1Q9(o1Fm|Nj9G5R=({E<`*rG%_(TV>mK1IW{sfW?^M!H8o>2 zH8Ey0Vq-RBGBq|nJTWvfF)(8|GBY_gGBRdiWo9)sV>C4}W;0@AHe@n2Ha?R}e}V#| Zmy^$b?E_LO%#*)>AP6)HB_%~qMhbx1MJ@mU delta 1626 zcmV-g2BrDvfdc1&03mppq9v~; z{XBa+&tKmcqv05a#jPw|G>}+by6_pYz2+#gaoR1qqsc98J#p}*!!P4IKDA8s(ALe< zH3to*b8aDrC|EpcF8x3r?Gg5D^@)E`=Plof#)<%`RPWDz5WNo&hf=zg(eWsM;8U}0 z_|ZdV1^LT>E7(GL+QV|MXs^0Pgo{4C{o~?vv;AXYl9fM8-CtyA}^bubf6$vR~70LR@0QfM*5+Bgdr1SI;A+Z$Ny@<$+Zd=UcirbEH zu_f_>#3~YH3Ws_{w}EKw1OY0urtK9tD*Gc^%E;?@U6%MZi3L9S9D0_Y;+ zxNbWs5zwIy+DHnujijIzkpdZ{zI>NF8WUw@X_qH~Zvy#_P z8~N;b-lLv8w{*sf`9l$Ob`4DL=q?d+=k!HBy!}!N9e=%jKkQ@nx5CH1PH5FQ)XuU) z>FEL(;fa8Co$La0?yz?}z8&R^XXm5t?VRp+h2iM^JN6V$FjJ3zC-eLXuYWvW`gvN} zN3;DF0;&3ZILZ|Z<))e#%?lRn2D=wSw9nWa)|4N<$exXRlRkzaOUg>R`d#Rs&>C#Z zmGoxvc07c1&aB!6VY8QQjOQ4sk!~&K|*0+=`=wUvU&`w)u%sF7*?H9sC4y zIs%dIowxXj-RdHLW`~PxWiRm(pL&Vl`~hyVt^0?1ieK{-{cumY_;7yw{_O$`e>_j2 zFDsT@g}(au6<^sprx+blU%{^SP|`O!^kvgO?JHaPYkWm;x$G?ZA#=)>{vYZrg0Fdt z`4J3}N|#rAk>ELf8&+l(=tCJ z1oaD5d@hoT`h_a;Z%IY|t<|{65)XTG9#YO7U*()9oQsC@kI&w|eSHdDSDPre`%-Sb zs!6v9x}^wz+{8TMah-Whu@}EU<^daMo)YiG7UB~Uv+3x5p8YSMO|r>2&x*e%*#L*y zY%u(qXZc_E-BChvYWTh!JGsQM+TqtmHyj^dzBv7KNvg~`#qO~v_M0dcHN^@O%PPfs znc^iPi!ZZ0FX@~QQ)f+#+nYwSM5A4y(aVO$kW3GM@6O&mzqx{@R~K%bNB>XrcqsG{ zuh$PRDl#2U*{sN?f$D0!p|??{C@pM9NZ0zi$`)c7ql|k$BpSN5P zOVou8)Fp(acI-=7;3;82tPo~G!XTKjmzr6eyXXXv_rBIK$$~d{n>y)gwMMT|l zpa;23U2k%C*Ue68xyjO1ddV!{|3420liGeRL?}5)GCW)|H#S@|GcsH$TrxH{Tr)N> zTrx2=eK<*FB~d9TIY}}+TrxK{Tr)E=Tq#^KHaA=|HZWW=F*SWSNo6HbDU(cpf&!bj YlhA+d15YZ;lfZx=2r~*LB}Gq03V3)&CjbBd