For this Monte-Carlo Tree Search python coding, why do I have RecursionError: maximum recursion depth exceeded while calling a Python object ?
Is this normal for MCTS which needs to keep expanding ?
Or did I miss any other bugs which I am still tracing at this moment ?
As for puct_array , see PUCT formula for explanation
import numpy as np
import random
# Reference :
# https://www.reddit.com/r/learnmachinelearning/comments/fmx3kv/empirical_example_of_mcts_calculation_puct_formula/
# PUCT formula : https://colab.research.google.com/drive/14v45o1xbfrBz0sG3mHbqFtYz_IrQHLTg#scrollTo=1VeRCpCSaHe3
# https://en.wikipedia.org/wiki/Monte_Carlo_tree_search#Exploration_and_exploitation
cfg_puct = np.sqrt(2) # to balance between exploitation and exploration
puct_array = [] # stores puct ratio for every child nodes for argmax()
# determined by PUCT formula
def find_best_path(parent):
if (parent == root) | (len(parent.nodes) == 0):
return parent
for N in parent.nodes:
puct_array.append(N.puct)
max_index = np.argmax(puct_array)
# leaf node has 0 child node
is_leaf_node = (len(parent.nodes[max_index].nodes) == 0)
if is_leaf_node:
return parent.nodes[max_index]
return parent.nodes[max_index]
class Mcts:
def __init__(self, parent):
# https://www.tutorialspoint.com/python_data_structure/python_tree_traversal_algorithms.htm
# https://www.geeksforgeeks.org/sum-parent-nodes-child-node-x/
self.parent = parent # this is the parent node
self.nodes = [] # creates an empty list with no child nodes initially
self.data = 0 # can be of any value, but just initialized to 0
self.visit = 1 # when a node is first created, it is counted as visited once
self.win = 0 # because no play/simulation had been performed yet
self.loss = 0 # because no play/simulation had been performed yet
self.puct = 0 # initialized to 0 because game had not started yet
# this function computes W/N ratio for each node
def compute_total_win_and_visits(self, total_win=0, visits=0):
if self.win:
total_win = total_win + 1
if self.visit:
visits = visits + 1
if self.nodes: # if there is/are child node(s)
for n in self.nodes: # traverse down the entire branch for each child node
n.compute_total_win_and_visits(total_win, visits)
return total_win, visits # same order (W/N) as in
# https://i.imgur.com/uI7NRcT.png inside each node
# Selection stage of MCTS
def select(self):
# traverse recursively all the way down from the root node
# to find the path with the highest W/N ratio (this ratio is determined using PUCT formula)
# and then select that leaf node to do the new child nodes insertion
leaf = find_best_path(self) # returns a reference pointer to the desired leaf node
leaf.insert() # this leaf node is selected to insert child nodes under it
# Expansion stage of MCTS
# Insert Child Nodes for a leaf node
def insert(self):
num_of_possible_game_states = 8 # assuming that we are playing tic-tac toe
for S in range(num_of_possible_game_states):
self.nodes.append(Mcts(self)) # inserts child nodes
self.nodes[len(self.nodes) - 1].simulate()
# Simulation stage of MCTS
def simulate(self):
# will replace the simulation stage with a neural network in the future
self.win = random.randint(0, 1) # just for testing purpose, so it is either win (1) or lose (0)
self.loss = ~self.win & random.randint(0, 1) # 'and' with randn() for tie/draw situation
self.backpropagation(self.win, self.loss)
# Backpropagation stage of MCTS
def backpropagation(self, win, loss):
# traverses upwards to the root node
# and updates PUCT ratio for each parent nodes
# computes the PUCT expression Q+U https://slides.com/crem/lc0#/9
if self.parent == 0:
num_of_parent_visits = 0
else:
num_of_parent_visits = self.parent.visit
total_win_for_all_child_nodes, num_of_child_visits = self.compute_total_win_and_visits(0, 0)
self.visit = num_of_child_visits
# traverses downwards all branches (only for those branches involved in previous play/simulation)
# and updates PUCT values for all their child nodes
self.puct = (total_win_for_all_child_nodes / num_of_child_visits) + \
cfg_puct * np.sqrt(num_of_parent_visits) / (num_of_child_visits + 1)
if self.parent == root: # already reached root node
self.select()
else:
self.parent.visit = self.parent.visit + 1
if win:
if self.parent.parent: # grandparent node (same-coloured player) exists
self.parent.parent.win = self.parent.parent.win + 1
if (win == 0) & (loss == 0): # tie is between loss (0) and win (1)
self.parent.win = self.parent.win + 0.5 # parent node (opponent player)
if self.parent.parent: # grandparent node (same-coloured player) exists
self.parent.parent.win = self.parent.parent.win + 0.5
self.parent.backpropagation(win, loss)
# Print the Tree
def print_tree(self, child):
for x in child.nodes:
print(x.data)
if x.nodes:
self.print_tree(x.nodes)
root = Mcts(0) # we use parent=0 because this is the head/root node
root.select()
print(root.print_tree(root))
Problem solved using
import sys
sys.setrecursionlimit(100000)
See the most up-to-date code here.
Related
PROBLEM
I'm writing a Monte-Carlo tree search algorithm to play chess in Python. I replaced the simulation stage with a custom evaluation function. My code looks perfect but for some reason acts strange. It recognizes instant wins easily enough but cannot recognize checkmate-in-2 moves and checkmate-in-3 moves positions. Any ideas?
WHAT I'VE TRIED
I tried giving it more time to search but it still cannot find the best move even when it leads to a guaranteed win in two moves. However, I noticed that results improve when I turn off the custom evaluation and use classic Monte Carlo Tree Search simulation. (To turn off custom evaluation, just don't pass any arguments into the Agent constructor.) But I really need it to work with custom evaluation because I am working on a machine learning technique for board evaluation.
I tried printing out the results of the searches to see which moves the algorithm thinks are good. It consistently ranks the best move in mate-in-2 and mate-in-3 situations among the worst. The rankings are based on the number of times the move was explored (which is how MCTS picks the best moves).
MY CODE
I've included the whole code because everything is relevant to the problem. To run this code, you may need to install python-chess (pip install python-chess).
I've struggled with this for more than a week and it's getting frustrating. Any ideas?
import math
import random
import time
import chess
import chess.engine
class Node:
def __init__(self, state, parent, action):
"""Initializes a node structure for a Monte-Carlo search tree."""
self.state = state
self.parent = parent
self.action = action
self.unexplored_actions = list(self.state.legal_moves)
random.shuffle(self.unexplored_actions)
self.colour = self.state.turn
self.children = []
self.w = 0 # number of wins
self.n = 0 # number of simulations
class Agent:
def __init__(self, custom_evaluation=None):
"""Initializes a Monte-Carlo tree search agent."""
if custom_evaluation:
self._evaluate = custom_evaluation
def mcts(self, state, time_limit=float('inf'), node_limit=float('inf')):
"""Runs Monte-Carlo tree search and returns an evaluation."""
nodes_searched = 0
start_time = time.time()
# Initialize the root node.
root = Node(state, None, None)
while (time.time() - start_time) < time_limit and nodes_searched < node_limit:
# Select a leaf node.
leaf = self._select(root)
# Add a new child node to the tree.
if leaf.unexplored_actions:
child = self._expand(leaf)
else:
child = leaf
# Evaluate the node.
result = self._evaluate(child)
# Backpropagate the results.
self._backpropagate(child, result)
nodes_searched += 1
result = max(root.children, key=lambda node: node.n)
return result
def _uct(self, node):
"""Returns the Upper Confidence Bound 1 of a node."""
c = math.sqrt(2)
# We want every WHITE node to choose the worst BLACK node and vice versa.
# Scores for each node are relative to that colour.
w = node.n - node.w
n = node.n
N = node.parent.n
try:
ucb = (w / n) + (c * math.sqrt(math.log(N) / n))
except ZeroDivisionError:
ucb = float('inf')
return ucb
def _select(self, node):
"""Returns a leaf node that either has unexplored actions or is a terminal node."""
while (not node.unexplored_actions) and node.children:
# Pick the child node with highest UCB.
selection = max(node.children, key=self._uct)
# Move to the next node.
node = selection
return node
def _expand(self, node):
"""Adds one child node to the tree."""
# Pick an unexplored action.
action = node.unexplored_actions.pop()
# Create a copy of the node state.
state_copy = node.state.copy()
# Carry out the action on the copy.
state_copy.push(action)
# Create a child node.
child = Node(state_copy, node, action)
# Add the child node to the list of children.
node.children.append(child)
# Return the child node.
return child
def _evaluate(self, node):
"""Returns an evaluation of a given node."""
# If no custom evaluation function was passed into the object constructor,
# use classic simulation.
return self._simulate(node)
def _simulate(self, node):
"""Randomly plays out to the end and returns a static evaluation of the terminal state."""
board = node.state.copy()
while not board.is_game_over():
# Pick a random action.
move = random.choice(list(board.legal_moves))
# Perform the action.
board.push(move)
return self._calculate_static_evaluation(board)
def _backpropagate(self, node, result):
"""Updates a node's values and subsequent parent values."""
# Update the node's values.
node.w += result.pov(node.colour).expectation()
node.n += 1
# Back up values to parent nodes.
while node.parent is not None:
node.parent.w += result.pov(node.parent.colour).expectation()
node.parent.n += 1
node = node.parent
def _calculate_static_evaluation(self, board):
"""Returns a static evaluation of a *terminal* board state."""
result = board.result(claim_draw=True)
if result == '1-0':
wdl = chess.engine.Wdl(wins=1000, draws=0, losses=0)
elif result == '0-1':
wdl = chess.engine.Wdl(wins=0, draws=0, losses=1000)
else:
wdl = chess.engine.Wdl(wins=0, draws=1000, losses=0)
return chess.engine.PovWdl(wdl, chess.WHITE)
def custom_evaluation(node):
"""Returns a static evaluation of a board state."""
board = node.state
# Evaluate terminal states.
if board.is_game_over(claim_draw=True):
result = board.result(claim_draw=True)
if result == '1-0':
wdl = chess.engine.Wdl(wins=1000, draws=0, losses=0)
elif result == '0-1':
wdl = chess.engine.Wdl(wins=0, draws=0, losses=1000)
else:
wdl = chess.engine.Wdl(wins=0, draws=1000, losses=0)
return chess.engine.PovWdl(wdl, chess.WHITE)
# Evaluate material.
material_balance = 0
material_balance += len(board.pieces(chess.PAWN, chess.WHITE)) * +100
material_balance += len(board.pieces(chess.PAWN, chess.BLACK)) * -100
material_balance += len(board.pieces(chess.ROOK, chess.WHITE)) * +500
material_balance += len(board.pieces(chess.ROOK, chess.BLACK)) * -500
material_balance += len(board.pieces(chess.KNIGHT, chess.WHITE)) * +300
material_balance += len(board.pieces(chess.KNIGHT, chess.BLACK)) * -300
material_balance += len(board.pieces(chess.BISHOP, chess.WHITE)) * +300
material_balance += len(board.pieces(chess.BISHOP, chess.BLACK)) * -300
material_balance += len(board.pieces(chess.QUEEN, chess.WHITE)) * +900
material_balance += len(board.pieces(chess.QUEEN, chess.BLACK)) * -900
# TODO: Evaluate mobility.
mobility = 0
# Aggregate values.
centipawn_evaluation = material_balance + mobility
# Convert evaluation from centipawns to wdl.
wdl = chess.engine.Cp(centipawn_evaluation).wdl(model='lichess')
static_evaluation = chess.engine.PovWdl(wdl, chess.WHITE)
return static_evaluation
m1 = chess.Board('8/8/7k/8/8/8/5R2/6R1 w - - 0 1') # f2h2
# WHITE can win in one move. Best move is f2-h2.
m2 = chess.Board('8/6k1/8/8/8/8/1K2R3/5R2 w - - 0 1')
# WHITE can win in two moves. Best move is e2-g2.
m3 = chess.Board('8/8/5k2/8/8/8/3R4/4R3 w - - 0 1')
# WHITE can win in three moves. Best move is d2-f2.
agent = Agent(custom_evaluation)
result = agent.mcts(m2, time_limit=30)
print(result)
I don't know how difficult is this question. Suppose we train a decision tree and then I want to records all nodes and leaves in the tree, what are their ancestors (set of p for parents), and which of their ancestors are divided based on x<a (set A_l) and x>a (set A_r).
Actually, nodes can be classified as follows and I want to extract all of this information:
t = 1,...,T set of all nodes
p(t): parent of node t
A(t): set of all ancestors of node t
A_L(t): the set of ancestors of t whose left branch has been followed on the path from the root node to t
A_R(t): the set of ancestors of t whose left branch has been followed on the path from the root node to t
T_b: the set of branch nodes
T_l: the set of leaf nodes
I wrote a code and I could store all left and right children of a node, but I don't know how to determine the aforementioned sets.
class Nodes():
no_nodes = 0
def __init__(self,tree,no_total_nodes,node):
self.no_nodes = no_total_nodes
self.current_node= node
self.left_child = all_left_children(tree,self.current_node)
self.right_child = all_right_children(tree,self.current_node)
where
def func_right_node(tree,start_node):
node = tree.children_right[start_node]
return node
def func_left_node(tree,start_node):
node = tree.children_left[start_node]
return node
def all_right_children(tree,start_node):
List=[]
while True:
r_node = func_right_node(tree,start_node)
if r_node!= -1:
List.append(r_node)
start_node = r_node
elif r_node== -1:
break
return List
def all_left_children(tree,start_node):
List=[]
while True:
r_node = func_left_node(tree,start_node)
if r_node!= -1:
List.append(r_node)
start_node = r_node
elif r_node== -1:
break
return List
and all nodes and leaves are stores as follows:
TREE = decision_tree.tree_
total_number_nodes = decision_tree.tree_.node_count
All_Nodes = []
All_Leaves = []
for i in range(total_number_nodes):
All_Nodes.append(Nodes(TREE,total_number_nodes,i))
if not All_Nodes[i].left_child:
All_Leaves.append(All_Nodes[i])
I have a graph like this:
# graph table
graph = {}
graph['start'] = {}
graph['start']['a'] = 5
graph['start']['b'] = 2
graph['a'] = {}
graph['a']['c'] = 4
graph['a']['d'] = 2
graph['b'] = {}
graph['b']['a'] = 8
graph['b']['d'] = 7
graph['c'] = {}
graph['c']['d'] = 6
graph['c']['finish'] = 3
graph['d'] = {}
graph['d']['finish'] = 1
graph['finish'] = {}
And I am trying to find the fastest way from S to F.
In the first example in the book only one edge was connected to one node, in this example for example, node D has 3 weights and a cost table was used:
costs = {}
infinity = float('inf')
costs['a'] = 5
costs['b'] = 2
costs['c'] = 4
costs['d'] = # there is 3 costs to node D, which one to select?
costs['finish'] = infinity
And a parents table:
parents = {}
parents['a'] = 'start' # why not start and b since both `S` and `B` can be `A` nodes parent?
parents['b'] = 'start'
parents['c'] = 'a'
parents['d'] = # node D can have 3 parents
parents['finish'] = None
But this also works, by works I mean no error is thrown, so do I only have to name the parents from the first node S?
parents = {}
parents['a'] = 'start'
parents['b'] = 'start'
parents['finish'] = None
The code:
processed = []
def find_lowest_cost_node(costs):
lowest_cost = float('inf')
lowest_cost_node = None
for node in costs:
cost = costs[node]
if cost < lowest_cost and node not in processed:
lowest_cost = cost
lowest_cost_node = node
return lowest_cost_node
node = find_lowest_cost_node(costs)
while node is not None:
cost = costs[node]
neighbors = graph[node]
for n in neighbors.keys():
new_cost = cost + neighbors[n]
if costs[n] > new_cost:
costs[n] = new_cost
parents[n] = node
processed.append(node)
node = find_lowest_cost_node(costs)
def find_path(parents, finish):
path = []
node = finish
while node:
path.insert(0, node)
if parents.__contains__(node):
node = parents[node]
else:
node = None
return path
path = find_path(parents, 'finish')
distance = costs['finish']
print(f'Path is: {path}')
print(f'Distance from start to finish is: {distance}')
I get:
Path is: ['finish']
Distance from start to finish is: inf
Where is my mistake and how should I add cost and parent to a node which can be visited from more than 1 node?
Edit
I do believe this is not the best approach for this problem, the best in practice solution / recommendations are welcome.
You do not need to initialise the cost table with more than costs['start'] = 0 or the parents dictionary with more than parents = {}. That is what your algorithm is going to create for you!
The only other change you need to make is to your while loop. It just needs to check whether the new node has already been detected before. If so then we check to see whether the new path is shorter and update as required; if not then we establish the new path.
while node is not None:
cost = costs[node]
neighbors = graph[node]
for n in neighbors.keys():
new_cost = cost + neighbors[n]
if n in costs:
if costs[n] > new_cost:
costs[n] = new_cost
parents[n] = node
else:
costs[n] = new_cost
parents[n] = node
processed.append(node)
node = find_lowest_cost_node(costs)
I think there are much neater ways to deal with graphs but this is the minimal change needed to make your code work as required. Hope it's helpful!
You will be adding to the classes Node and Tree that we developed in our lectures. There are several short methods that you will have to write.
Write a method is_similar() that takes as input two binary trees and returns true if the nodes have the same key values and are arranged in the same order and false otherwise.
def is_similar (self, pNode):
Write a method print_level() that takes as input the level and prints out all the nodes at that level. If that level does not exist for that binary search tree it prints nothing. Use the convention that the root is at level 1.
def print_level (self, level):
Write a method get_height() that returns the height of a binary tree. Recall that the height of a tree is the longest path length from the root to a leaf.
def get_height (self):
Write a method num_nodes() that returns the number of nodes in the left subtree from the root and the number of nodes in the right subtree from the root and the root itself. This function will be useful to determine if the tree is balanced.
def num_nodes (self):
Input: The input will read from a file. The file will be formatted as follows:
Line 1: Several integers separated by spaces to be inserted into Tree 1
Line 2: Several integers separated by spaces to be inserted into Tree 2
You will read both lines of data. Create two Trees and insert the integers in the order given. Then you will use these two trees to test the methods that you have written.
Output: The output will be formatted as follows:
The Trees are similar: (True or False)
Levels of Tree 1:
print each level on its own line
Levels of Tree 2:
print each level on its own line
Height of Tree 1:
Nodes in Tree 1:
Height of Tree 2:
Nodes in Tree 2:
For example, given the following input file:
14 17 1
14 17 1
This would be the output:
The Trees are similare: True
Levels of Tree 1:
14
1 17
Levels of Tree 2:
14
1 17
Height of Tree 1: 1
Nodes in Tree 1: 3
Height of Tree 2: 1
Nodes in Tree 2: 3
You will be writing helper methods for the Tree class that we developed. The following is the outline of the code that you will be submitting. You may include other functions that we developed for completeness. You may add helper functions as needed.
Below is the code that I need help finishing. Not entirely sure how to start the helper functions or main so any help would be appreciated.
import os
class Node (object):
def __init__ (self, data):
self.data = data
self.lchild = None
self.rchild = None
class Tree (object):
def __init__ (self):
self.root = None
# insert data into the tree
def insert (self, data):
new_node = Node (data)
if (self.root == None):
self.root = new_node
return
else:
current = self.root
parent = self.root
while (current != None):
parent = current
if (data < current.data):
current = current.lchild
else:
current = current.rchild
# found location now insert node
if (data < parent.data):
parent.lchild = new_node
else:
parent.rchild = new_node
# Returns true if two binary trees are similar
def is_similar (self, pNode):
pass
# Prints out all nodes at the given level
def print_level (self, level):
pass
# Returns the height of the tree
def get_height (self):
pass
# Returns the number of nodes in tree which is
# equivalent to 1 + number of nodes in the left
# subtree + number of nodes in the right subtree
def num_nodes (self):
pass
def main():
# write code here
main()
As a hint, think of how you would need to traverse the binary tree in the implementation of each helper method.
For num_nodes, I am not sure what "and the number of nodes in the right subtree from the root and the root itself." means. Should we return the number of nodes in the right subtree + 1?
#classmethod
def count_below(node):
count=0
if (node == None):
return 0 # if one of the root's childs was None
if (node.lchild == None and node.rchild == None): # leaf
return 1 # base case
if (node.lchild != None):
count+=count_below(node.lchild)
if (node.rchild != None):
count+=count_below(node.rchild)
return count
def num_nodes(self):
if (self.root == None):
return 0
return count_below(self.root.lchild), count_below(self.root.rchild) + 1
#classmethod
def depth_below(node):
if node is None:
return 0 # base case
# Compute the depth of each subtree
ldepth = depth_below(node.lchild) # recurse left
rdepth = depth_below(node.rchild) # recurse right
# once all the recursive calls performed on this node's childs resolve
# return the depth of the subtree of this node with the greater depth
if (ldepth > rdepth):
return ldepth+1
else:
return rdepth+1
def get_height(self):
return depth_below(self.root) # depth from root
#classmethod
def explore_childs(node, current_level, target_level):
if (node.lchild==None and node.rchild==None):
return # base case
if (current_level == target_level):
if (node.lchild!=None):
print(node.lchild.data)
if (node.rchild!=None):
print(node.rchild.data)
return # base case
if (node.lchild!=None):
explore_childs(node.lchild, current_level+1, target_level) # recurse left
if (node.rchild!=None):
explore_childs(node.rchild, current_level+1, target_level) # recurse right
def print_level(self, level):
if (level > self.get_height()):
pass # throw error
explore_childs(root, 0, level)
I am trying to implement an A-star search algorithm to find a path in a square maze from (0,0) to (dimension - 1, dimension - 1). My algorithm returns the correct path when it exists; however, if there is no path, then it runs in an infinite loop. How do I fix this? For now, I've put a condition to run if the length of the open list exceeds (dimension ^ 4), but this is obviously not a permanent fix. I am using Python 3.7.3.
import numpy as np
class node():
def __init__(self, parent=None, location=None):
self.parent = parent
self.location = location
self.g = float(0)
self.h = float(0)
self.f = float(0)
#returns euclidean distance between two nodes
#takes the locations/tuples of two nodes as arguments
#works properly
def euclidean_distance(node_1, node_2):
return float((((node_2[1] - node_1[1])**2) + ((node_2[0] - node_1[0])**2))**0.5)
#to make extracting the value at a given location in the maze easier
#takes the maze and two integers as arguments
def get_value(maze, a, b):
return maze[a][b]
def out_of_bounds(a, b, dim):
return (a < 0 or a >= dim) or (b < 0 or b >= dim)
#Euclidean A* Search, takes the maze and dimension as arguments
def a_star_euclidean(maze, dim):
#initializing start node and end node
start = node(None, (0,0))
end = node(None, (dim-1, dim-1))
#initializing open list and closed list
open_list = []
closed_list = []
open_list.append(start)
while len(open_list) > 0:
#assigning currentNode
currentNode = open_list[0]
currentNode_index = 0
#current location
for index, item in enumerate(open_list):
if item.f < currentNode.f:
currentNode = item
currentNode_index = index
#(currentNode.location)
row = currentNode.location[0]
column = currentNode.location[1]
#updating open list and closed list
open_list.pop(currentNode_index)
closed_list.append(currentNode)
#in case goal node is already reached
if currentNode.location == end.location:
path = []
current = currentNode
while current is not None:
path.append(current.location)
current = current.parent
#return path[::-1] #returning the path from start to end
path.reverse()
return path
else:
closed_list.append(currentNode)
#generating childs
child_locations = [(row+1, column), (row-1, column), (row, column+1), (row, column-1)]
#print(child_locations)
child_nodes = [node(currentNode, location) for location in child_locations]
#print(child_nodes)
for child in child_nodes:
#declaring row and column variables for child nodes
child_row = int(child.location[0])
child_column = int(child.location[1])
if not out_of_bounds(child_row, child_column, dim):
# Child is on the closed list
if child in open_list:
continue
#computing g(n), h(n), f(n)
child.g = float(currentNode.g + 1)
child.h = float(euclidean_distance(child.location, end.location))
child.f = float(child.g + child.h)
#child is in open list
if child in closed_list:
continue
if get_value(maze, child_row, child_column) == 0:
open_list.append(child)
else:
continue
else:
continue
#if (len(open_list) > dim**4): #b^d worst case
#return None
def main():
maze = []
dim = int(input("Enter the dimension of the game: "))
print(dim)
for row in range(dim):
maze.append([])
for column in range(dim):
maze[row].append(int(np.random.binomial(1, 0.3, 1)))
maze[0][0] = 0
maze[dim-1][dim-1] = 0
print(maze)
print("----------")
print(a_star_euclidean(maze,dim))
#print(euclidean_distance((1,1), (2,2)))
main()
I believe the issue is that child in closed_list is never true, because you haven't overriden the __eq__ operator of the node class. Because of this, python doesn't know how to compare two instances of the node class, so falls back to comparing if they are references to the same object in memory, otherwise it returns false. So two nodes are never equal when searching through closed_list.
Try defining the __eq__ operator for the node class like so. You can change the comparison to include other properties as you need.
class node():
def __init__(self, parent=None, location=None):
self.parent = parent
self.location = location
self.g = float(0)
self.h = float(0)
self.f = float(0)
def __eq__(self, other):
return self.location == other.location