feat: 2109s ps1
This commit is contained in:
parent
c46a9ee24e
commit
01ffa3b6aa
295
cs2109s/labs/ps2/cube.py
Normal file
295
cs2109s/labs/ps2/cube.py
Normal file
@ -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
|
BIN
cs2109s/labs/ps2/graph.png
Normal file
BIN
cs2109s/labs/ps2/graph.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
855
cs2109s/labs/ps2/ps2.ipynb
Normal file
855
cs2109s/labs/ps2/ps2.ipynb
Normal file
File diff suppressed because one or more lines are too long
442
cs2109s/labs/ps2/ps2.py
Normal file
442
cs2109s/labs/ps2/ps2.py
Normal file
@ -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?
|
94
cs2109s/labs/ps2/utils.py
Normal file
94
cs2109s/labs/ps2/utils.py
Normal file
@ -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)
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user