def get_best_meld_combins_from_arr(cls, arr: list) -> list: sorted_arr = arr[:] sorted_arr.sort() tree = Tree() identifier = 'root' tree.create_node([], identifier) MjMath.get_melds_tree_from_sorted_arr(sorted_arr, tree, identifier) paths = tree.paths_to_leaves() _combinations = [] for path in paths: _combin = [] for identifier in path: meld = tree.get_node(identifier).tag _combin.append(meld) _combin.sort() if _combin not in _combinations: _combinations.append(_combin) len_arr = [len(x) for x in _combinations] max_len = max(len_arr) best_combinations = [] for _combin in _combinations: if len(_combin) == max_len: best_combinations.append(_combin) return best_combinations
def find_deepest_child(data): tree = Tree() root = tree.create_node("root", "root") tree = build_tree(data=data, tree=tree, parent=root) res = tree.paths_to_leaves() depth = tree.depth() for path in res: if len(path) == depth + 1: return path[-1] break
def _prune(tree: treelib.Tree, retain: Set[str]) -> treelib.Tree: res = treelib.Tree() res.create_node(tree[tree.root].tag, tree.root, None, tree[tree.root].data) for path in tree.paths_to_leaves(): if path[-1] in retain: for i, node in enumerate(path[1:]): if node not in res: curr = tree[node] res.create_node(curr.tag, node, res[path[i]], curr.data) return res
def spaces(self, output="dictionary"): spaces = self.get_all_spaces() page_labels = self.get_page_labels() spaces_tree = Tree() #Add PDF name as top node spaces_tree.create_node(self.file_name, self.file_name) #Add pages nodes for key, value in page_labels.items(): spaces_tree.create_node(value, value, parent=self.file_name) spaces_tree = self.spacestree(spaces[key], spaces_tree, value) if output == "tree": return (spaces_tree) elif output == "hierarchy": #Getting a master space hierarchy by column #Max depth starts calculating outside the doc name and sheet name max_depth = max([len(i) for i in spaces_tree.paths_to_leaves()]) - 2 space_depth = [] for level in range(max_depth): space_depth.append("Space " + str(level + 1)) #Create the dictionary to hold the values space_dict = {} for idx, level in enumerate(space_depth): space_dict[level] = [] #Loop through all space items for item in spaces_tree.paths_to_leaves(): for level, space in enumerate(item[2:]): res = list(space_dict.keys())[level] if str(space)[7:-6] not in space_dict[res]: space_dict[res].append(str(space)[7:-6]) return (space_dict) else: # Return the Python dictionary spaces_dict = {'spaces': []} spaces_dict['spaces'].append(spaces_tree.to_dict()) return (spaces_dict)
def pack(): root = tk.Tk() root.withdraw() dialogPath = filedialog.askdirectory() if not dialogPath.strip(): print('您已取消打包') return 'cancleDialog' packPath = os.path.join(dialogPath, 'package') zipPath = os.path.join(dialogPath, 'zip') if os.path.exists(packPath): shutil.rmtree(packPath) os.makedirs(packPath) if os.path.exists(zipPath): shutil.rmtree(zipPath) # print(packPath) print('打包中...') leafPathList = Tree.paths_to_leaves(addFileTree) for pathList in leafPathList: curLeafPath = filePath targetLeafPath = packPath for path in pathList: if path != 'id': pathName = addFileTree.get_node(path).tag[:-(len(path) + 2)] curLeafPath = os.path.join(curLeafPath, pathName) targetLeafPath = os.path.join(targetLeafPath, pathName) # print(curLeafPath) # print(targetLeafPath) targetLeafPath = targetLeafPath.replace('/', '\\') if curLeafPath.find('.') == -1: shutil.copytree(curLeafPath, targetLeafPath) else: targetLeafParent = os.path.split(targetLeafPath)[0] if os.path.exists(targetLeafParent): shutil.copy(curLeafPath, targetLeafPath) else: os.makedirs(targetLeafParent) shutil.copy(curLeafPath, targetLeafPath) print('\n打包完成,开始压缩文件...') os.makedirs(zipPath) outZipPath = os.path.join(zipPath, 'dist.zip') zipDir(packPath, outZipPath) print('\n打包完成,模块输出路径:' + zipPath + '\dist.zip') return 'success'
def getAllSubPathOfTree(file_name): global index_node index_node = 0 parent_node = "" tree = Tree() root = ET.parse(file_name).getroot() walkData(root, parent_node, tree) treePaths_id_list = tree.paths_to_leaves() #树的数据路径 treePaths_list = [] for paths in treePaths_id_list : path_list = [] for i in range(1, paths.__len__()): path = paths[i] path = removeLastIntegerNumber(path) if path.endswith('true', path.__len__() - 4, path.__len__()): path_list.append(path) treePaths_list.append(path_list) return treePaths_list
def getAllSubPathOfTree(file_name): global index_node index_node = 0 parent_node = "" tree = Tree() root = ET.parse(file_name).getroot() walkData(root, parent_node, tree) treePaths_id_list = tree.paths_to_leaves() #映射成为树的节点的ID形成的路径,和树的数据的路径还不大一样 treePaths_list = [] for paths in treePaths_id_list: path_list = [] for path in paths: path_list.append(removeLastIntegerNumber(path)) treePaths_list.append(path_list) ## 这里要处理include的属性 filePath = os.path.dirname(os.path.realpath(file_name)) #获取当前文件所在文件夹 resTreePaths_list = addIncludeXMlTree(treePaths_list, filePath) # print(treePaths_list) return resTreePaths_list
class GradientBoostingTree(object): def __init__(self, X, y, k=4, epochs=200, loss='mse', metrics=['mae'], learning_rate=0.001, layers=3, ending_units=256, optimizers=[Adam, Nadam, RMSprop], early_stop=None, seed=None): self.X = X self.y = y self.k = k self.epochs = epochs self.loss = loss self.metrics = metrics self.learning_rate = learning_rate self.layers = layers self.ending_units = ending_units self.optimizers = optimizers self.early_stop = early_stop self.generator = secrets.SystemRandom(seed) self.models = [] self.tree = Tree() def fit(self): y = self.y preds = np.zeros(len(y)) def fit_leaf(preds, y, i): optimizer = self.optimizers[self.generator.randint( 0, len(self.optimizers) - 1)](learning_rate=self.learning_rate) model = network_builder(layers=self.layers, ending_units=self.ending_units) model.compile(loss=self.loss, optimizer=optimizer, metrics=self.metrics) model.fit(self.X, y, epochs=self.epochs * (i + 1), callbacks=[self.early_stop] if self.early_stop else []) new_preds = preds + model.predict(self.X).reshape(-1) new_y = self.y - new_preds return new_preds, new_y, model def fit_tree(k, preds, y, i, parent=None): if k > 0: l_preds, l_y, l_model = fit_leaf(preds, y, i) self.tree.add_node(Node(identifier=parent + 'l' + str(k), data=l_model), parent=parent) fit_tree(k - 1, l_preds, l_y, i + 1, parent + 'l' + str(k)) r_preds, r_y, r_model = fit_leaf(preds, y, i) self.tree.add_node(Node(identifier=parent + 'r' + str(k), data=r_model), parent=parent) fit_tree(k - 1, r_preds, r_y, i + 1, parent + 'r' + str(k)) i = 0 preds, y, root_model = fit_leaf(preds, y, i) self.tree.add_node(Node(identifier='root', data=root_model)) fit_tree(self.k, preds, y, i + 1, parent='root') def predict(self, X): preds = [] leafs = self.tree.leaves('root') for path in self.tree.paths_to_leaves(): p = .0 for id in path: p += self.tree.get_node(id).data.predict(X) preds.append(p) return sum(preds) / len(leafs)
class TreeT(object): def __init__(self, max_id=0): self.tree = Tree() def from_ptb_to_tree(self, line, max_id=0, leaf_id=1, parent_id=None): # starts by ['(', 'pos'] pos_tag = line[1] if parent_id is None: pos_id = 0 else: pos_id = max_id max_id += 1 self.tree.create_node(pos_tag, pos_id, parent_id, TreeData()) parent_id = pos_id total_offset = 2 if line[2] != '(': # sub-tree is leaf # line[0:3] = ['(', 'pos', 'word', ')'] word_tag = line[2] self.tree.create_node(word_tag, leaf_id, parent_id, TreeData()) return 4, max_id, leaf_id + 1 line = line[2:] while line[0] != ')': offset, max_id, leaf_id = self.from_ptb_to_tree( line, max_id, leaf_id, parent_id) total_offset += offset line = line[offset:] return total_offset + 1, max_id, leaf_id def add_height(self, tree_dep): for n in self.tree.all_nodes(): n.data.leaves = [] for leaf in self.tree.leaves(): lid = leaf.identifier hid = tree_dep[lid] if hid == self.tree.root: self.tree[lid].data.height = self.tree.depth(self.tree[lid]) for cid in [ p for p in self.tree.paths_to_leaves() if lid in p ][0]: self.tree[cid].data.leaves += [lid] else: height = -1 cid = lid cond = True while cond: self.tree[cid].data.leaves += [lid] height += 1 cid = self.tree.parent(cid).identifier cid_leaves = [l.identifier for l in self.tree.leaves(cid)] cid_l_dep = [tree_dep[l] for l in cid_leaves if l != lid] cond = set(cid_l_dep).issubset(set(cid_leaves)) self.tree[lid].data.height = height x_nodes = [ n.identifier for n in self.tree.all_nodes() if n.data.leaves == [] ] for x_node in x_nodes[::-1]: min_id = min(self.tree.children(x_node), key=lambda c: c.data.height) _lid = min_id.data.leaves[0] self.tree[_lid].data.height += 1 self.tree[x_node].data.leaves += [_lid] return True def _from_tree_to_ptb(self, nid): nid = self.tree.subtree(nid).root if self.tree[nid].is_leaf(): return ' (' + self.tree[nid].tag + ' ' + self.tree[ nid].data.word + ')' res = ' (' + self.tree[nid].tag for c_nid in sorted(self.tree.children(nid), key=lambda x: x.identifier): res += self._from_tree_to_ptb(c_nid.identifier) return res + ')' def from_tree_to_ptb(self): return self._from_tree_to_ptb(self.tree.root) def from_tag_to_tree(self, tag, word, pos_id=0): parent_id = None for tag_nodes in tag: if tag_nodes[0] in [CL, CR]: c_side = tag_nodes[0] _tag_nodes = tag_nodes[1:] if len(tag_nodes) > 1 else [''] else: c_side = '' _tag_nodes = tag_nodes self.tree.create_node(_tag_nodes[0], pos_id, parent=parent_id, data=TreeData(comb_side=c_side)) parent_id = pos_id pos_id += 1 for tag_node in _tag_nodes[1:]: self.tree.create_node(tag_node[1:], pos_id, parent=parent_id, data=TreeData(miss_side=tag_node[0])) pos_id += 1 for l in self.tree.leaves(): if l.data.miss_side == '': l.data.word = word break return pos_id @memoize def is_combine_to(self, side): return self.tree[self.tree.root].data.comb_side == side @memoize def is_combine_right(self): return self.is_combine_to(CR) @memoize def is_combine_left(self): return self.is_combine_to(CL) @memoize def is_complete_tree(self): return all([n.data.miss_side == '' for n in self.tree.all_nodes()]) @memoize def get_missing_leaves_to(self, miss_val, side): return [ l.identifier for l in self.tree.leaves(self.tree.root) if l.data.miss_side == side and l.tag == miss_val ] @memoize def get_missing_leaves_left(self, miss_val): return self.get_missing_leaves_to(miss_val, L) @memoize def get_missing_leaves_right(self, miss_val): return self.get_missing_leaves_to(miss_val, R) @memoize def root_tag(self): return self.tree[self.tree.root].tag @memoize def is_no_missing_leaves(self): return all( [l.data.miss_side == '' for l in self.tree.leaves(self.tree.root)]) @memoize def combine_tree(self, _tree, comb_leaf): self.tree.paste(comb_leaf, _tree.tree) self.tree.link_past_node(comb_leaf) return self def tree_to_path(self, nid, path): # Stop condition if self.tree[nid].is_leaf(): path[nid] = [] return nid, self.tree[nid].data.height # Recursion flag = CR for child in self.tree.children(nid): cid = child.identifier leaf_id, height = self.tree_to_path(cid, path) if (height == 0): # Reached end of path can add flag path[leaf_id].insert(0, flag) # path[leaf_id].append(flag) if height > 0: path[leaf_id].insert(0, nid) # only single child will have height>0 # and its value will be the one that is returned # to the parent ret_leaf_id, ret_height = leaf_id, height - 1 # once we reached a height>0, it means that # this path includes the parent, and thus flag # direction should flip flag = CL return ret_leaf_id, ret_height def path_to_tags(self, path): tags = [] for p in path: _res = [] _p = copy.copy(p) if _p[0] in [CL, CR]: _res.append(_p[0]) _p = _p[1:] while _p[:-1]: el_p = _p.pop(0) _res.append(self.tree[el_p].tag) for c in self.tree.children(el_p): if c.identifier != _p[0]: _res.append(R + c.tag if c.identifier > _p[0] else L + c.tag) _res.append(self.tree[_p[0]].tag) tags.append(_res) return tags def path_to_words(self, path): return [self.tree[k].tag for k in path] def from_tree_to_tag(self): path = {} self.tree_to_path(self.tree.root, path) return { 'tags': self.path_to_tags(path.values()), 'words': self.path_to_words(path.keys()) } def from_ptb_to_tag(self, line, max_id, depend): self.from_ptb_to_tree(line, max_id) self.add_height(depend) path = {} self.tree_to_path(self.tree.root, path) return self.path_to_tags(path.values())
from Helper.Source import connect_to_db Conn_Odin = connect_to_db() # Submission of interest AllSubmissions = pd.read_sql_query("select * from Submission_Info", Conn_Odin) AllSubmissions[["ID_Submission", "Title"]].tail(60) SubmissionOfInterest = "kboh0h" Temp_Query = "select CI.ID_Comment, CI.ID_ParentID, CI.created_utc from Comment_Information CI " +\ "where ID_Submission= '{}'".format(SubmissionOfInterest) AllComments = pd.read_sql_query(Temp_Query, Conn_Odin) AllComments2 = AllComments.copy() AllComments2["created_utc"] = pd.to_datetime(AllComments2.created_utc) AllComments2 = AllComments2.sort_values("created_utc").reset_index(drop=True) tree1 = Tree() tree1.create_node(identifier=SubmissionOfInterest) # root node for CommentIndex in range(len(AllComments2)): # CommentIndex=0 ChildID = AllComments2.iloc[CommentIndex]["ID_Comment"] ParentID = AllComments2.iloc[CommentIndex]["ID_ParentID"] try: tree1.create_node(identifier=ChildID, parent=ParentID) except: print("An exception occurred") tree1.show() tree1.paths_to_leaves() len(tree1.paths_to_leaves())
unique_ids = set(nodeids) tree.create_node(tag=rootid,identifier=rootid) while len(unique_ids) != len(tree.all_nodes()): for rightNode in list(parent_check.keys()): if tree.get_node(parent_check[rightNode]) is not None and tree.get_node(rightNode) is None: tree.create_node(tag=rightNode, identifier=rightNode, parent=tree.get_node(parent_check[rightNode])) for node in tree.all_nodes(): sum += tree.depth(node) print(sum) def intersection(lst1, lst2): lst3 = [value for value in lst1 if value in lst2] return lst3 sanIndex = 0 youInex = 0 for path in tree.paths_to_leaves(): if path[len(path) -1] == "SAN": sanIndex = path[path.index('7LD'):] elif path[len(path) - 1] == "YOU": youIndex = path[path.index('7LD'):] print (len(sanIndex) + len(youIndex) - 4)
def find_path(startpose, endpose, path_step): #tree DST used to store path solutions. #each path is uniquely denoted by its leaf pointer. soln_tree = Tree() root = soln_tree.create_node(data=startpose) # root :) def path_from_leaf(node): path = [node.data] while not node.is_root(): predecessor = node.predecessor(soln_tree.identifier) path.append(soln_tree.get_node(predecessor).data) node = soln_tree.get_node(predecessor) path.reverse() return path frontier = [root] next_frontier = [] pass_ct = 0 #let rocket start upside down, but remove cases where it's #upside down once it's righted itself righted = False while frontier: pose_node = frontier.pop() current_pose = pose_node.data def create_node(data): return soln_tree.create_node(data=data, parent=pose_node) if random.random() < (1 / 1000): path = path_from_leaf(pose_node) x = [s.x for s in path] y = [s.y for s in path] plt.plot(x, y, linewidth=2, color='blue') plt.savefig(f"plot{random.random()}.png") plt.close() #bruteforce with large set of P, G rs = RocketSimulation(current_pose.x, current_pose.y, current_pose.dx, current_pose.dy, current_pose.theta, current_pose.dtheta) next_states = [ rs.clone().update_n(p, g, path_step) for p in np.arange(0, 1, 0.05) for g in np.arange(-0.2, 0.2, 0.05) ] def normalize_angle(angle): angle = angle % (2 * math.pi) angle = (angle + (2 * math.pi)) % (2 * math.pi) if angle > math.pi: angle -= (2 * math.pi) return angle def collision_filter(pose): L = 40 W = 2 s = math.sin(pose.theta) c = math.cos(pose.theta) outer_points = [ Vector2(0, L / 2), Vector2(1.8 * W, -(L / 2) - W), Vector2(-1.8 * W, -(L / 2) - W) ] for p in outer_points: if ((p.x * s) + (p.y * c) + pose.y) < 0: return False return True def orientation_filter(pose): if abs(normalize_angle(pose.theta)) <= math.pi / 4: return True else: return not righted if not righted: for state in next_states: if abs(normalize_angle(state.theta)) <= math.pi / 4: righted = True #filter states filters = [ lambda pose: pose.y > 0, #lambda pose: pose_dist(startpose, endpose) > pose_dist(pose, endpose), #closer #lambda pose: abs(normalize_angle(pose.theta)) <= math.pi / 4, #upside-down bad collision_filter, orientation_filter ] next_states = list( filter(lambda x: all(f(x) for f in filters), next_states)) correct_solns = list( filter(lambda pose: pose_eq(pose, endpose), next_states)) if (correct_solns): print("Correct solutions found!!!") return [ path_from_leaf(create_node(soln)) for soln in correct_solns ] next_states = sorted(next_states, key=lambda x: pose_dist(x, endpose), reverse=False) #try promising cases first if random.random() <= 1 / 500: print( f"Produced {len(next_states)} states during pass (t={pass_ct * path_step * 0.02})" ) if len(next_states) != 0: print( f"Closest distance: {pose_dist(next_states[0], endpose)}") if len(next_states) != 0: #always choose the best soln, also choose 2 random members next_frontier.append(create_node(next_states.pop())) #next_frontier.extend( # [create_node(x) for x in random.sample(next_states[:30], 3)] #) next_frontier.extend([ create_node(x) for x in random.sample(next_states, min(len(next_states), 2)) ]) if not frontier: if not next_frontier: print("No next frontier! Exiting!") frontier = next_frontier next_frontier = [] print( f"Round {pass_ct} completed. Produced {len(frontier)} states.") pass_ct += 1 #generate summary plots if (pass_ct != 0) and (pass_ct % 15 == 0): all_paths = filter(lambda x: len(x) == pass_ct + 1, soln_tree.paths_to_leaves()) for identifier_list in all_paths: node_list = [ soln_tree.get_node(id) for id in identifier_list ] pose_list = [pose_node.data for pose_node in node_list] x = [s.x for s in pose_list] y = [s.y for s in pose_list] plt.plot(x, y, linewidth=2, color='blue') plt.savefig(f"summary_plt{random.random()}.png") plt.close() #perform larger culling step in case of overfull frontier if len(frontier) > 4000: #Bucketization buckets = defaultdict(list) #bucket sizes bx, by, btheta = 2.5, 2.5, 0.5 for f in frontier: buckets[hash( (math.floor(f.data.x / bx), math.floor(f.data.y / by), math.floor(f.data.theta / btheta)))].append(f) #pick representatives from buckets new_frontier = [] for bucket, nodes in buckets.items(): #pick ideal and random candidate sorted_poses = sorted(nodes, key=lambda node: weigthed_pose_dist( node.data, endpose)) new_frontier.append(sorted_poses[0]) if len(sorted_poses) > 1: #make sure same candidate isnt chosen twice sorted_poses.pop(0) new_frontier.extend( random.sample(sorted_poses, min(len(sorted_poses), 2))) #re-add any exceptional cases #for node in sorted_poses: # if node.data.y < 85: # new_frontier.append(node) print( f"Bucketization: removed {len(frontier) - len(new_frontier)}/{len(frontier)} elements" ) frontier = new_frontier print(f"frontier: {frontier}\nnext_frontier: {next_frontier}") return []
class HFE(FeatureTransformChoice): def __init__(self, dataset_shape, taxonomy=None): self.dataset_shape = dataset_shape self.taxonomy = taxonomy def fit(self, hyperparameters, X, y): # Initialize new tree self.tree = Tree() if isinstance(self.taxonomy, type(None)): print( 'Warning: Could not execute HFE algorithm no taxonomy provided' ) else: # Add otus to the internal tree structure for i, otu in enumerate( [a for a in X.columns.values if a.lower().startswith('otu')]): self._add_otu_to_tree(self.tree, otu, X.copy()) # Perform filtering then determine which nodes in the tree will act as the final features self._correlation_filter() self._path_ig_filter(y) self._leaf_ig_filter(y) self.valid_ids = [ x.identifier for x in self.tree.all_nodes() if x.data['valid'] ] def transform(self, X, y=None): # Build a new tree (Note: The self.tree is built on training data, so we need a new tree for unseen data) new_tree = Tree() for i, otu in enumerate( [a for a in X.columns.values if a.lower().startswith('otu')]): self._add_otu_to_tree(new_tree, otu, X.copy()) # Extract the final features and store in dataframe result = pd.DataFrame() for i, current_id in enumerate(self.valid_ids): result[i] = new_tree[current_id].data['feature_vector'] return result def get_name(self): return 'HFE' def hyperparameter_grid(self): return None # Description: Populates tree structure def _add_otu_to_tree(self, tree, otu_name, otu_table): raw_string = self.taxonomy['Taxonomy'][otu_name.lower() == np.array( [x.lower() for x in self.taxonomy['OTU']])].values[0] taxonomic_string = re.sub(r'[()0-9]', '', raw_string) taxonomic_levels = taxonomic_string.split(';') feature_vector = otu_table.loc[:, otu_name].values.copy() for i, level in enumerate(taxonomic_levels): # Some of the levels might be empty if there an extra ; in the taxonomy file or two back to back. These should be skipped if level == '': continue if tree.contains(level): # Increment node OTU vector with argument OTU vector (ie node vector + new otu vector) tree[level].data['feature_vector'] += feature_vector.copy() else: # Create new node with dictionary of OTU vector and boolean flag indicating valid node tree.create_node( tag=level, identifier=level, parent=taxonomic_levels[i - 1] if i != 0 else None, data={ 'feature_vector': feature_vector.copy(), 'valid': True }) # Description: Removes all child nodes whose feature vector is correlated with their parent node by switching the valid flag for that node to False def _correlation_filter(self, threshold=0.80): paths = self.tree.paths_to_leaves() for path in paths: if len(path) < 1: continue for i in range(1, len(path)): parent_feature_vector = self.tree[path[ -1 - i + 1]].data['feature_vector'] child_feature_vector = self.tree[path[ -1 - i]].data['feature_vector'] current_correlation = pearsonr(parent_feature_vector, child_feature_vector)[0] if current_correlation > threshold: self.tree[path[-1 - i]].data['valid'] = False # Description: Filters nodes based on average path information gain (IG) def _path_ig_filter(self, labels): paths = self.tree.paths_to_leaves() for path in paths: added = 0 avg_path_IG = None for i, bacteria in enumerate(path): # For thoses nodes that passed the correlation filter compute the running IG average if self.tree[bacteria].data['valid']: if added == 0: avg_path_IG = mutual_info_classif( X=self.tree[bacteria].data['feature_vector']. reshape(len(labels), 1), y=labels, random_state=0) added += 1 else: avg_path_IG = avg_path_IG * (added / (added + 1)) + ( mutual_info_classif(X=self.tree[bacteria].data[ 'feature_vector'].reshape(len(labels), 1), y=labels, random_state=0) / (added + 1)) added += 1 # If a node in the path is less than the average or it is uninformative (ie all zeros) remove the node for bacteria in path: current_bacteria_IG = mutual_info_classif( X=self.tree[bacteria].data['feature_vector'].reshape( len(labels), 1), y=labels, random_state=0) if (avg_path_IG == None) or (current_bacteria_IG < avg_path_IG) or sum( self.tree[bacteria].data['feature_vector']) == 0: self.tree[bacteria].data['valid'] = False # Description: Filter leaf nodes in incomplete paths based on global information gain (IG) def _leaf_ig_filter(self, labels): # Returns whether a path is incomplete def incomplete_path(path): incomplete = False for bacteria in path: if not self.tree[bacteria].data['valid']: incomplete = True return incomplete paths = self.tree.paths_to_leaves() avg_tree_IG = None added = 0 # Compute global avg IG for path in paths: for bacteria in path: # Only the remaining nodes contribute to global avg IG if self.tree[bacteria].data['valid']: if added == 0: avg_tree_IG = mutual_info_classif( X=self.tree[bacteria].data['feature_vector']. reshape(len(labels), 1), y=labels, random_state=0) added += 1 else: avg_tree_IG = avg_tree_IG * (added / (added + 1)) + ( mutual_info_classif(X=self.tree[bacteria].data[ 'feature_vector'].reshape(len(labels), 1), y=labels, random_state=0) / (added + 1)) added += 1 # The leaf nodes are the final element in the path. If the leaf node IG is zero or less than global IG and is a part of an incomplete path it should be removed for path in paths: leaf_node_IG = mutual_info_classif( X=self.tree[path[-1]].data['feature_vector'].reshape( len(labels), 1), y=labels, random_state=0) if incomplete_path(path) and ((leaf_node_IG == 0) or (leaf_node_IG < avg_tree_IG)): self.tree[path[-1]].data['valid'] = False
class MonteCarlo: N_THREADS = 1 PERCENTILE = 100 def __init__(self, engine=None, hero=None): # self.last_ev = 0 # self.rolling_10 = deque(maxlen=10) # self.rolling_40 = deque(maxlen=40) self.ev_history = {} self.time_start = None self.duration = None self.queue = None self.leaf_path = None if not engine: # logger.info('engine not given, loading from file...') self.engine_checksum = None self.load_engine(hero) else: # logger.info('engine given') self.init(engine, hero) @property def current_actions(self): return [(c.data['action'], c.data['ev'], c.data['traversed']) for c in self.tree.children(self.tree.root)] def is_time_left(self): return time.time() - self.time_start < self.duration @retrace.retry(on_exception=(EOFError, KeyError), interval=0.1, limit=None) def load_engine(self, hero): with shelve.open(Engine.FILE) as shlv: if shlv['hash'] != self.engine_checksum: # logger.info('loading engine from file...') self.engine_checksum = shlv['hash'] self.init(shlv['engine'], hero) def init(self, engine, hero): # logger.info('init state') self.engine = engine self.hero = hero or self.engine.q[0][0] self.hero_pocket = self.engine.data[self.hero]['hand'] for s in self.engine.data: self.ev_history[s] = deque(maxlen=50) # logger.info('HERO is at seat {} with {}'.format(self.hero, self.hero_pocket)) self.watched = False self.init_tree() def init_tree(self): """create the tree. Add a root; available action will add the first level of children""" # self.traversed_ceiling = 1 self.tree = Tree() root = self.tree.create_node('root', identifier='root', data={'traversed': 0, 'ev': 0, 'stats': 1, 'cum_stats': 1}) # # logger.info('tree:\n{}'.format(self.tree.show())) # input('new tree') def watch(self): """Runs when engine file changes. Just kicks off run for 3s sprints""" # logger.info('Monte Carlo watching every {}s...'.format(self.timeout)) while True: # loads new engine file if checksum changed self.load_engine() # do not analyze if game finished if self.engine.phase in [self.engine.PHASE_SHOWDOWN, self.engine.PHASE_GG]: if not self.watched: # logger.error('game is finished') self.watched = True time.sleep(3) continue # do not analyze if hero does not have pocket if self.hero_pocket in [['__', '__'], [' ', ' ']]: if not self.watched: # logger.error('hero does not have a pocket') self.watched = True time.sleep(0.5) continue # do not analyze if hero is not to play if self.hero != self.engine.q[0][0]: if not self.watched: # logger.error('hero is not to act') self.watched = True time.sleep(0.5) continue if self.is_complete: if not self.watched: # logger.error('mc is complete') self.watched = True time.sleep(2) continue # run a few sims # logger.debug('running now with timeout {}'.format(self.timeout)) self.run() self.timeout += 0.1 def run(self, duration): """Run simulations For x: - clone engine - start at root -- iterate and find next unprocessed node -- action engine to that node parent -- process that node - keep processing - with return EV Levelling: extremely huge iterations when many players. So do the most probably actions only till all done. Handling close action approximations: """ # logger.info('Monte Carlo started') total_traversions_start = sum(a[2] for a in self.current_actions) # cannot run if engine in showdown or gg if self.engine.phase in [self.engine.PHASE_SHOWDOWN, self.engine.PHASE_GG]: logger.warning('cannot run mc with no actions') return self.duration = duration self.time_start = time.time() self.queue = PriorityQueue() # threads = [] # for _ in range(self.N_THREADS): # t = MCWorker(self) # # t.start() # threads.append(t) # self.traversed_focus = 0 leaves = self.tree.paths_to_leaves() # logger.debug('leaves from tree: {}'.format(len(leaves))) # leaves.sort(key=lambda lp: len(lp) + sum(int(lpn.split('_')[0]) for lpn in lp), reverse=True) # # logger.debug('{} leaves are now sorted by formula'.format(len(leaves))) # logger.debug('{}'.format(json.dumps(leaves[:3], indent=4, default=str))) # leaves.sort(key=len) # logger.debug('{} leaves are now sorted by length'.format(len(leaves))) # logger.debug('{}'.format(json.dumps(leaves[:3], indent=4, default=str))) # leaves.sort(key=lambda lp: int(lp[-1][:3]), reverse=True) # logger.debug('{} leaves are now sorted by rank'.format(len(leaves))) # logger.error(json.dumps(leaves, indent=4, default=str)) # input('>>') for leaf_path in leaves: node = self.tree[leaf_path[-1]] item = ( 1 - node.data['cum_stats'], leaf_path, ) self.queue.put(item) # for t in threads: # t.start() # # for t in threads: # t.join() # if t.error: # raise Exception().with_traceback(t.error[2]) while self.is_time_left() and not self.queue.empty(): priority, self.leaf_path = self.queue.get_nowait() self.run_item(self.leaf_path) if self.queue.empty(): logger.info(f'Everything was processed in queue!') total_traversions_end = sum(a[2] for a in self.current_actions) if total_traversions_end <= total_traversions_start: logger.warning(f'No new traversion added to {total_traversions_start}') def run_item(self, path): # logger.debug('running this path: {}'.format(path)) e = deepcopy(self.engine) e.mc = True """To calculate the investment for the loss EV, the total amounts used till end is required. Cannot use final player balance on engine as that might have winnings allocated to it by the engine. Instead the difference from all the new matched bets from the current matched bets will be used. Need to add current contrib """ e.matched_start = e.data[self.hero]['matched'] + e.data[self.hero]['contrib'] # logger.info('hero starting with matched = {} from {} + {}'.format( # e.matched_start, e.data[self.hero]['matched'], e.data[self.hero]['contrib'])) # self.tree.show() self.fast_forward(e, path) # logger.info('{}'.format('-' * 200)) # input('check item') def show_best_action(self): """Calculates best action on root""" # logger.error("\n\n") sum_traversed = 0 delta = 0 max_ev = float('-inf') action = None amount = None for nid in self.tree[self.tree.root].fpointer: child = self.tree[nid] # logger.debug('{} {}'.format(child.tag, child.data)) dat = child.data sum_traversed += dat['traversed'] # logger.error('{} @{} => {}'.format(dat['action'], dat['traversed'], round(dat['ev'], 4))) # delta += abs(1 - (self.convergence.get(dat['action'], 1) / dat['ev'] if dat['ev'] else 1)) # self.convergence[dat['action']] = dat['ev'] if dat['ev'] > max_ev: max_ev = dat['ev'] action = dat['action'] if action.startswith('bet') or action.startswith('raise') or action.startswith('allin'): amount = dat['amount'] best_action = '{}{}'.format(action, ' with {}'.format(amount) if amount else '') # self.convergence['deq'].append(round(delta, 1)) self.convergence['deq'].append(best_action) # # logger.error('deq: {}'.format(list(self.convergence['deq']))) # logger.error('') # logger.error('Timeout: {}'.format(round(self.timeout, 1))) # logger.error('Traversed: {}'.format(sum_traversed)) deq_cnts = Counter(list(self.convergence['deq'])) # # logger.error('deq: {}'.format(deq_cnts.most_common())) # logger.error('{}% for {}'.format( # 100 * sum(dq == deq_list[-1] for dq in deq_list[:-1]) // (len(deq_list) - 1) # 100 * (deq_cnts.most_common()[0][1] - deq_cnts.most_common()[1][1]) // self.convergence_size # if len(deq_cnts) > 1 else 100 * len(self.convergence['deq']) // self.convergence_size, # deq_cnts.most_common()[0][0] # )) def fast_forward(self, e, path): """Do actions on engine till the leaf is reached. Need to do available_actions before every DO First check if the leave is already processed, then skip this path. When the leaf is reached then process from that node. Remember to send through only the first letter for the action. Then update the nodes from this leaf back up the tree """ # logger.info('Fast forwarding {} nodes'.format(len(path))) if len(path) == 1: # logger.info('processing root for first time') self.process_node(e, self.tree[path[0]]) return leaf_node = self.tree[path[-1]] # logger.debug('checking if last node has been processed:') # logger.debug('last node leaf {} has node data {}'.format(leaf_node.tag, leaf_node.data)) if leaf_node.data['traversed']: # logger.info('This leaf node ({}) above focus level {}'.format(leaf_node.tag, self.traversed_focus)) # can happen as all actions are added, but then one was chosen to continue on # and that path for that action wasn't removed from the queue return for nid in path[1:]: node = self.tree[nid] # logger.debug('fast forwarding action for node {}'.format(node.tag)) e.available_actions() cmd = [node.data['action'][0]] if 'amount' in node.data: cmd.append(node.data['amount']) # logger.debug('Adding bet value of {}'.format(node.data['amount'])) # logger.debug('Executing path action {} for {}'.format(cmd, node.tag)) # logger.debug('Executing path action {} with data {}'.format(cmd, node.data)) e.do(cmd) if node.is_leaf(): # logger.debug('{} is a leaf node, processing next...'.format(node.tag)) self.process_node(e, node) logger.info('nodes processed, now updating nodes that were fast forwarded') for processed_nid in reversed(path[1:]): processed_node = self.tree[processed_nid] self.update_node(processed_node) self.ev_history[self.engine.s].append(sum(a[1] for a in self.current_actions)) def process_node(self, e, n): """Process node Get actions available for node Pick action to traverse with UCT Process action selected Return EV """ # logger.info('processing node {} with data {}'.format(n.tag, n.data)) # this node is the hero folding (to prevent this being processed as leaf) # was created with other children (but not most probable at that time to be proc as child) # if hero folding, then make this node a leaf node with fold eq # exiting before adding children alleviates the need to remove the immediately again thereafter # bug: cannot use engine.q as it already rotated after taking action getting here if not n.is_root() and n.data['action'] == 'fold' and self.hero == n.data['seat']: winnings, losses = self.net(e) result = { 'ev': losses, 'traversed': 1, } # logger.info('hero has folded this node given: {}'.format(result)) n.data.update(result) # logger.info('node data after fold: {}'.format(n.data)) return # add the children of the node if not n.fpointer: self.add_actions(e, n) # this node is a leaf (no more actions to take!) # either the game finished and we have winner and pot # or we have to use pokereval.winners if n.is_leaf(): # logger.info('node {} is the final action in the game'.format(n.tag)) # winner given (easy resolution) if e.winner: # logger.debug('engine gave winner {}'.format(e.winner)) winnings, losses = self.net(e) ev = winnings if self.hero in e.winner else losses # else if the winner is unknown # then calculate winners and use # percentage of hero as amt else: if 'in' not in e.data[self.hero]['status']: # hero fold is handled before in method # and thus for equities calc it is just 0 # logger.debug('Hero {} is not in game'.format(self.hero)) ev = 0 else: winnings, losses = self.net(e) equities = PE.showdown_equities(e) # equities = self.get_showdown_equities(e) ev_pos = winnings * equities[self.hero] # logger.debug('ev_pos = {} from winnings {} * eq {}'.format(ev_pos, winnings, equities[self.hero])) ev_neg = losses * (1 - equities[self.hero]) # logger.debug('ev_neg = {} from losses {} * -eq {}'.format(ev_neg, losses, (1 - equities[self.hero]))) ev = ev_pos + ev_neg logger.info('Net EV: {} from {} + {}'.format(ev, ev_pos, ev_neg)) result = { 'ev': ev, 'traversed': 1, } # logger.info('{} leaf has result {}'.format(n.tag, result)) n.data.update(result) return # node is all good (not leaf (has children) and not hero folding) # get child actions and process most probable action a_node = self.most_probable_action(n) action = a_node.data['action'] # logger.info('taking next child node action {}'.format(action)) # if it is hero and he folds, # it is not necessarily an immediate ZERO equity # since my previous contrib needs to be added to the pot (i.e. contribs after starting mc) # i.e. make this a leaf node implicitly # no child nodes to remove for fold if action == 'fold' and self.hero == a_node.data['seat']: winnings, losses = self.net(e) result = { 'ev': losses, 'traversed': 1, } # logger.info('hero has folded the child node selected: {}'.format(result)) a_node.data.update(result) # logger.info('a_node data after: {}'.format(a_node.data)) # else we must process the node else: # logger.info('taking action {} and processing that node'.format(action)) cmd = [action[0]] if 'amount' in a_node.data: cmd.append(a_node.data['amount']) # logger.debug('Adding bet value of {}'.format(a_node.data['amount'])) e.do(cmd) self.process_node(e, a_node) # action node has been processed, now update node self.update_node(n) def update_node(self, node): """Update the node's data If leaf, then it was already calculated during processing, and now do not change it: the ev is the ev Minimax applied, hero pick best and foe picks min after p Traversed will stay the traversed_focus level for leaves, but for parent nodes the traversed will be the number of leaves reached from that node. """ is_hero = node.data.get('seat') == self.hero # logger.debug('is hero? {}'.format(is_hero)) # it will traverse back up to the root # root can be skipped if node.is_root(): # input('hero {} node data {}'.format(self.hero, node.data.get('seat'))) # if is_hero: # self.rolling_10.append(abs(self.last_ev)) # self.rolling_40.append(abs(self.last_ev)) # logger.debug('Added {} ev to collection'.format(self.last_ev)) # input('Added {} ev to collection'.format(self.last_ev)) # logger.debug('reached the root') # self.update_ev_change() return # fast forwarding will send here, just ignore node if leaf if node.is_leaf(): # logger.debug('not updating {}: it is final game result (no leaf nodes)'.format(node.tag)) # logger.debug('not updating {}: final data {}'.format(node.tag, node.data)) return depth = self.tree.depth(node) # logger.info('updating node {} at depth {}'.format(node.tag, depth)) # logger.info('node has {} before update'.format(node.data)) if not len(node.fpointer): # logger.error('node {} with {} as no children...'.format(node.tag, node.data)) raise Exception('not necessary to process leaves') # logger.debug('extracting data from {} children nodes...'.format(len(node.fpointer))) n_ev = float('-inf') if is_hero else 0 n_traversed = 0 for child_nid in node.fpointer: child_node = self.tree[child_nid] # logger.debug('child node {} has {}'.format(child_node.tag, child_node.data)) dat = child_node.data if not dat['traversed']: # logger.debug('skipping untraversed {}'.format(child_node.tag)) continue # get max for hero if is_hero: # todo is this +ev dampening necessary # todo this should be fixed when setting for hand range # equities = PE.showdown_equities(self.engine) # n_ev = max(n_ev, dat['ev'] * equities.get(self.hero, 0)) n_ev = max(n_ev, dat['ev']) # get min for foe else: # ev_adj = dat['ev'] * dat['stats'] # logger.debug('foe min between {} and {}'.format(n_ev, ev_adj)) # n_ev = min(n_ev, ev_adj) n_ev += dat['ev'] * dat['stats'] / dat['divider'] n_traversed += dat['traversed'] # logger.debug('added {} traversed: now have {} so far'.format(dat['traversed'], n_traversed)) self.last_ev = node.data['ev'] - n_ev node.data.update({ 'ev': n_ev, 'traversed': n_traversed, }) # logger.info('now node has {} ev~{} after {}'.format(node.tag, round(n_ev, 3), n_traversed)) if not node.data['traversed']: raise Exception('node cannot be untraversed') def net(self, e): """Stored the balance at the start of sim. Now calculate difference as player total matched contrib. Winnings will be less initial starting contrib. """ e.gather_the_money() p = e.players[self.hero] d = e.data[self.hero] matched_diff = d['matched'] - e.matched_start # logger.debug('matched diff = {} from {} - {}'.format(matched_diff, d['matched'], e.matched_start)) winnings = int(e.pot - matched_diff) # logger.debug('winnings diff = {} from pot {} less matched {}'.format(winnings, e.pot, matched_diff)) losses = int(-matched_diff) # logger.info('Winnings = {} and losses = {}'.format(winnings, losses)) return winnings, losses def most_probable_action(self, parent): """All nodes will be processed once at least but it will never happen. Just return the most probable node for most accurate play. Using stats fields on data There should not be any untraversed nodes. So first get untraversed, then sort and pop first one""" # logger.info('getting most probable action after {}'.format(parent.tag)) children = self.tree.children(parent.identifier) children = [c for c in children if not c.data['traversed']] if not children: raise MonteCarloError('Cannot choose most probable action when all nodes are traversed') children.sort(key=lambda c: c.data['stats'], reverse=True) child = children[0] # logger.debug('{} is untraversed, returning that node for actioning'.format(child.tag)) self.leaf_path.append(child.identifier) return child def add_actions(self, e, parent): """Add actions available to this node If in GG phase then no actions possible, ever. Remove 'hand' Bets: - preflop are 2-4x BB - postflop are 40-100% pot Raise: - always double Allin: - only on river - if out of money then converted to allin Scale non-fold probabilities even though it should not have an effect. """ # logger.info('adding actions to {}'.format(parent.tag)) actions = e.available_actions() s, p = e.q[0] d = e.data[s] balance_left = p['balance'] - d['contrib'] if not actions: # logger.warn('no actions to add to node') return if 'gg' in actions: # logger.debug('no actions available, got gg') return actions.remove('hand') # remove fold if player can check if 'check' in actions: actions.remove('fold') # # logger.debug('removed fold when check available') # remove fold for hero # if s == self.hero and 'fold' in actions: # actions.remove('fold') # # logger.debug('removed fold from hero') # remove raise if player has already been aggressive if 'raise' in actions and any(pa['action'] in 'br' for pa in d[e.phase]): actions.remove('raise') # # logger.debug('removed raise as player has already been aggressive') # remove allin, but add it later with final stats (if increased from bet/raised) if 'allin' in actions: actions.remove('allin') # logger.debug('removed allin by default') # load stats (codes with counts) stats = ES.player_stats(e, s) max_contrib = max(pd['contrib'] for pd in e.data.values()) # contrib_short = max_contrib - d['contrib'] # allin needs to be the doc count # where bets and raises result in allin, add those prob dists to this # that will give proper probability go_allin = stats['actions'].get('a', 0) # # logger.info('filtered actions: {}'.format(actions)) # ev 0 instead of none because of root node sum when not all traversed it gives error action_nodes = [] for a in actions: node_data = { 'stats': stats['actions'].get(ACTIONS_TO_ABBR[a], 0.01), 'divider': 1, 'action': a, 'phase': e.phase, 'seat': s, 'name': p['name'], 'traversed': 0, 'ev': 0, } if a in ['bet', 'raise']: btps_and_amts = [] total_pot = sum(pd['contrib'] for pd in e.data.values()) + e.pot # for preflop only do 2x and 3x if e.phase == e.PHASE_PREFLOP: btps_and_amts.append(('double', e.bb_amt * 2)) btps_and_amts.append(('triple', e.bb_amt * 3)) # else do half and full pots else: btps_and_amts.append(('half_pot', total_pot * 0.50)) btps_and_amts.append(('full_pot', total_pot * 1.00)) # round bets up to a BB # btps_and_amts = [(btp, -(amt // -e.bb_amt) * e.bb_amt) # for btp, amt in btps_and_amts] betting_info = [] amts_seen = [] for btp, amt in btps_and_amts: if amt in amts_seen: # logger.debug('already using {}, skipping duplicate'.format(amt)) continue if a == 'bet' and amt < e.bb_amt: # logger.debug('bet cannot be less than BB {}'.format(e.bb_amt)) continue if a == 'raise' and amt < (max_contrib * 2): # logger.debug('raise cannot be less than 2x contrib of {}'.format(max_contrib * 2)) continue betting_info.append((btp, amt)) amts_seen.append(amt) # change raises that cause allin betting_info_final = [] for btp, amt in betting_info: # if amt is more than player balance, it is an allin if amt >= balance_left: go_allin += node_data['stats'] / len(betting_info) else: betting_info_final.append((btp, amt)) # all good, can have this bet as option for btp, amt in betting_info_final: node_data_copy = deepcopy(node_data) node_data_copy['divider'] = len(betting_info_final) node_data_copy['action'] = f'{a}_{btp}' node_data_copy['amount'] = amt action_nodes.append(node_data_copy) else: action_nodes.append(node_data) # allin will have doc counts (from stat, maybe from bets, maybe from raise) if go_allin: node_data = { 'stats': go_allin, 'divider': 1, 'action': 'allin', 'phase': e.phase, 'seat': s, 'name': p['name'], 'traversed': 0, 'ev': 0, 'amount': balance_left, } action_nodes.append(node_data) # logger.debug('added allin to actions with stat {}'.format(node_data['stats'])) # scale the stats (it is currently term counts aka histogram) and it is required to be # a probability distribution (p~1) # Also, certain actions like fold can be removed, and the total stats is not 1 total_stats = sum(an['stats'] / an['divider'] for an in action_nodes) for action_node in action_nodes: action_node['stats'] = max(0.01, action_node['stats'] / action_node['divider'] / total_stats) action_node['cum_stats'] = parent.data['cum_stats'] * action_node['stats'] node_tag = f'{action_node["action"]}_{s}_{e.phase}' identifier = f'{node_tag}_{str(uuid.uuid4())[:8]}' self.tree.create_node(identifier=identifier, tag=node_tag, parent=parent.identifier, data=action_node) # logger.debug('new {} for {} with data {}'.format(node_tag, s, action_node)) item = ( 1 - action_node['cum_stats'], self.leaf_path + [identifier] ) self.queue.put(item) # logger.debug('new {} for {} with data {}'.format(node_tag, s, action_node)) # logger.info('{} node actions added'.format(len(action_nodes))) def analyze_tree(self): """Analyze tree to inspect best action from ev""" # self.tree.show() # check all finished paths for path in self.tree.paths_to_leaves(): # skip untraversed end last_node = self.tree[path[-1]] if not last_node.data['traversed']: logger.debug('skipping untraversed endpoint {}'.format(last_node.tag)) continue # show all actions for nid in path: node = self.tree[nid] d = node.data logger.info('Node: {} ev={}'.format(node.tag, d['ev'])) 0/0 input('$ check tree') def get_showdown_equities(self, e): """instead of using pokereval, use hs from se""" hss = {} for s, d in e.data.items(): if 'in' in d['status']: hss[s] = ES.showdown_hs(e, s, percentile=self.PERCENTILE) # calculate for hero if self.hero in hss: d = e.data[self.hero] hss[self.hero] = PE.hand_strength(d['hand'], e.board, e.rivals) # normalize total = sum(hs for hs in hss.values()) equities = {s: hs / total for s, hs in hss.items()} return equities
# Build the orbit tree tree = Tree() tree.create_node("COM", "COM") to_treat_nodes = ["COM"] while len(to_treat_nodes) != 0: current_node = to_treat_nodes.pop() for orbit, center in orbit_map.items(): if center == current_node: tree.create_node(orbit, orbit, parent=center) to_treat_nodes.append(orbit) pertinent_paths = [ path for path in tree.paths_to_leaves() if path[-1] in ["YOU", "SAN"] ] # Get lowest common ancestor dsitance from COM lowest_common_ancestor = 0 for idx, pair in enumerate(zip(*pertinent_paths)): if pair[0] != pair[1]: lowest_common_ancestor = idx break # Calculate node distance between YOU and SAN distance = len(pertinent_paths[0]) + len( pertinent_paths[1]) - 2 * lowest_common_ancestor - 2 print("The number of orbit jumps to join Santa is {0}".format(distance))
class MonteCarlo: N_THREADS = 1 PERCENTILE = 100 def __init__(self, engine=None, hero=None): # self.last_ev = 0 # self.rolling_10 = deque(maxlen=10) # self.rolling_40 = deque(maxlen=40) self.ev_history = {} self.time_start = None self.duration = None self.queue = None self.leaf_path = None if not engine: # logger.info('engine not given, loading from file...') self.engine_checksum = None self.load_engine(hero) else: # logger.info('engine given') self.init(engine, hero) @property def current_actions(self): return [(c.data['action'], c.data['ev'], c.data['traversed']) for c in self.tree.children(self.tree.root)] def is_time_left(self): return time.time() - self.time_start < self.duration @retrace.retry(on_exception=(EOFError, KeyError), interval=0.1, limit=None) def load_engine(self, hero): with shelve.open(Engine.FILE) as shlv: if shlv['hash'] != self.engine_checksum: # logger.info('loading engine from file...') self.engine_checksum = shlv['hash'] self.init(shlv['engine'], hero) def init(self, engine, hero): # logger.info('init state') self.engine = engine self.hero = hero or self.engine.q[0][0] self.hero_pocket = self.engine.data[self.hero]['hand'] for s in self.engine.data: self.ev_history[s] = deque(maxlen=50) # logger.info('HERO is at seat {} with {}'.format(self.hero, self.hero_pocket)) self.watched = False self.init_tree() def init_tree(self): """create the tree. Add a root; available action will add the first level of children""" # self.traversed_ceiling = 1 self.tree = Tree() root = self.tree.create_node('root', identifier='root', data={ 'traversed': 0, 'ev': 0, 'stats': 1, 'cum_stats': 1 }) # # logger.info('tree:\n{}'.format(self.tree.show())) # input('new tree') def watch(self): """Runs when engine file changes. Just kicks off run for 3s sprints""" # logger.info('Monte Carlo watching every {}s...'.format(self.timeout)) while True: # loads new engine file if checksum changed self.load_engine() # do not analyze if game finished if self.engine.phase in [ self.engine.PHASE_SHOWDOWN, self.engine.PHASE_GG ]: if not self.watched: # logger.error('game is finished') self.watched = True time.sleep(3) continue # do not analyze if hero does not have pocket if self.hero_pocket in [['__', '__'], [' ', ' ']]: if not self.watched: # logger.error('hero does not have a pocket') self.watched = True time.sleep(0.5) continue # do not analyze if hero is not to play if self.hero != self.engine.q[0][0]: if not self.watched: # logger.error('hero is not to act') self.watched = True time.sleep(0.5) continue if self.is_complete: if not self.watched: # logger.error('mc is complete') self.watched = True time.sleep(2) continue # run a few sims # logger.debug('running now with timeout {}'.format(self.timeout)) self.run() self.timeout += 0.1 def run(self, duration): """Run simulations For x: - clone engine - start at root -- iterate and find next unprocessed node -- action engine to that node parent -- process that node - keep processing - with return EV Levelling: extremely huge iterations when many players. So do the most probably actions only till all done. Handling close action approximations: """ # logger.info('Monte Carlo started') total_traversions_start = sum(a[2] for a in self.current_actions) # cannot run if engine in showdown or gg if self.engine.phase in [ self.engine.PHASE_SHOWDOWN, self.engine.PHASE_GG ]: logger.warning('cannot run mc with no actions') return self.duration = duration self.time_start = time.time() self.queue = PriorityQueue() # threads = [] # for _ in range(self.N_THREADS): # t = MCWorker(self) # # t.start() # threads.append(t) # self.traversed_focus = 0 leaves = self.tree.paths_to_leaves() # logger.debug('leaves from tree: {}'.format(len(leaves))) # leaves.sort(key=lambda lp: len(lp) + sum(int(lpn.split('_')[0]) for lpn in lp), reverse=True) # # logger.debug('{} leaves are now sorted by formula'.format(len(leaves))) # logger.debug('{}'.format(json.dumps(leaves[:3], indent=4, default=str))) # leaves.sort(key=len) # logger.debug('{} leaves are now sorted by length'.format(len(leaves))) # logger.debug('{}'.format(json.dumps(leaves[:3], indent=4, default=str))) # leaves.sort(key=lambda lp: int(lp[-1][:3]), reverse=True) # logger.debug('{} leaves are now sorted by rank'.format(len(leaves))) # logger.error(json.dumps(leaves, indent=4, default=str)) # input('>>') for leaf_path in leaves: node = self.tree[leaf_path[-1]] item = ( 1 - node.data['cum_stats'], leaf_path, ) self.queue.put(item) # for t in threads: # t.start() # # for t in threads: # t.join() # if t.error: # raise Exception().with_traceback(t.error[2]) while self.is_time_left() and not self.queue.empty(): priority, self.leaf_path = self.queue.get_nowait() self.run_item(self.leaf_path) if self.queue.empty(): logger.info(f'Everything was processed in queue!') total_traversions_end = sum(a[2] for a in self.current_actions) if total_traversions_end <= total_traversions_start: logger.warning( f'No new traversion added to {total_traversions_start}') def run_item(self, path): # logger.debug('running this path: {}'.format(path)) e = deepcopy(self.engine) e.mc = True """To calculate the investment for the loss EV, the total amounts used till end is required. Cannot use final player balance on engine as that might have winnings allocated to it by the engine. Instead the difference from all the new matched bets from the current matched bets will be used. Need to add current contrib """ e.matched_start = e.data[self.hero]['matched'] + e.data[ self.hero]['contrib'] # logger.info('hero starting with matched = {} from {} + {}'.format( # e.matched_start, e.data[self.hero]['matched'], e.data[self.hero]['contrib'])) # self.tree.show() self.fast_forward(e, path) # logger.info('{}'.format('-' * 200)) # input('check item') def show_best_action(self): """Calculates best action on root""" # logger.error("\n\n") sum_traversed = 0 delta = 0 max_ev = float('-inf') action = None amount = None for nid in self.tree[self.tree.root].fpointer: child = self.tree[nid] # logger.debug('{} {}'.format(child.tag, child.data)) dat = child.data sum_traversed += dat['traversed'] # logger.error('{} @{} => {}'.format(dat['action'], dat['traversed'], round(dat['ev'], 4))) # delta += abs(1 - (self.convergence.get(dat['action'], 1) / dat['ev'] if dat['ev'] else 1)) # self.convergence[dat['action']] = dat['ev'] if dat['ev'] > max_ev: max_ev = dat['ev'] action = dat['action'] if action.startswith('bet') or action.startswith( 'raise') or action.startswith('allin'): amount = dat['amount'] best_action = '{}{}'.format( action, ' with {}'.format(amount) if amount else '') # self.convergence['deq'].append(round(delta, 1)) self.convergence['deq'].append(best_action) # # logger.error('deq: {}'.format(list(self.convergence['deq']))) # logger.error('') # logger.error('Timeout: {}'.format(round(self.timeout, 1))) # logger.error('Traversed: {}'.format(sum_traversed)) deq_cnts = Counter(list(self.convergence['deq'])) # # logger.error('deq: {}'.format(deq_cnts.most_common())) # logger.error('{}% for {}'.format( # 100 * sum(dq == deq_list[-1] for dq in deq_list[:-1]) // (len(deq_list) - 1) # 100 * (deq_cnts.most_common()[0][1] - deq_cnts.most_common()[1][1]) // self.convergence_size # if len(deq_cnts) > 1 else 100 * len(self.convergence['deq']) // self.convergence_size, # deq_cnts.most_common()[0][0] # )) def fast_forward(self, e, path): """Do actions on engine till the leaf is reached. Need to do available_actions before every DO First check if the leave is already processed, then skip this path. When the leaf is reached then process from that node. Remember to send through only the first letter for the action. Then update the nodes from this leaf back up the tree """ # logger.info('Fast forwarding {} nodes'.format(len(path))) if len(path) == 1: # logger.info('processing root for first time') self.process_node(e, self.tree[path[0]]) return leaf_node = self.tree[path[-1]] # logger.debug('checking if last node has been processed:') # logger.debug('last node leaf {} has node data {}'.format(leaf_node.tag, leaf_node.data)) if leaf_node.data['traversed']: # logger.info('This leaf node ({}) above focus level {}'.format(leaf_node.tag, self.traversed_focus)) # can happen as all actions are added, but then one was chosen to continue on # and that path for that action wasn't removed from the queue return for nid in path[1:]: node = self.tree[nid] # logger.debug('fast forwarding action for node {}'.format(node.tag)) e.available_actions() cmd = [node.data['action'][0]] if 'amount' in node.data: cmd.append(node.data['amount']) # logger.debug('Adding bet value of {}'.format(node.data['amount'])) # logger.debug('Executing path action {} for {}'.format(cmd, node.tag)) # logger.debug('Executing path action {} with data {}'.format(cmd, node.data)) e.do(cmd) if node.is_leaf(): # logger.debug('{} is a leaf node, processing next...'.format(node.tag)) self.process_node(e, node) logger.info( 'nodes processed, now updating nodes that were fast forwarded' ) for processed_nid in reversed(path[1:]): processed_node = self.tree[processed_nid] self.update_node(processed_node) self.ev_history[self.engine.s].append( sum(a[1] for a in self.current_actions)) def process_node(self, e, n): """Process node Get actions available for node Pick action to traverse with UCT Process action selected Return EV """ # logger.info('processing node {} with data {}'.format(n.tag, n.data)) # this node is the hero folding (to prevent this being processed as leaf) # was created with other children (but not most probable at that time to be proc as child) # if hero folding, then make this node a leaf node with fold eq # exiting before adding children alleviates the need to remove the immediately again thereafter # bug: cannot use engine.q as it already rotated after taking action getting here if not n.is_root( ) and n.data['action'] == 'fold' and self.hero == n.data['seat']: winnings, losses = self.net(e) result = { 'ev': losses, 'traversed': 1, } # logger.info('hero has folded this node given: {}'.format(result)) n.data.update(result) # logger.info('node data after fold: {}'.format(n.data)) return # add the children of the node if not n.fpointer: self.add_actions(e, n) # this node is a leaf (no more actions to take!) # either the game finished and we have winner and pot # or we have to use pokereval.winners if n.is_leaf(): # logger.info('node {} is the final action in the game'.format(n.tag)) # winner given (easy resolution) if e.winner: # logger.debug('engine gave winner {}'.format(e.winner)) winnings, losses = self.net(e) ev = winnings if self.hero in e.winner else losses # else if the winner is unknown # then calculate winners and use # percentage of hero as amt else: if 'in' not in e.data[self.hero]['status']: # hero fold is handled before in method # and thus for equities calc it is just 0 # logger.debug('Hero {} is not in game'.format(self.hero)) ev = 0 else: winnings, losses = self.net(e) equities = PE.showdown_equities(e) # equities = self.get_showdown_equities(e) ev_pos = winnings * equities[self.hero] # logger.debug('ev_pos = {} from winnings {} * eq {}'.format(ev_pos, winnings, equities[self.hero])) ev_neg = losses * (1 - equities[self.hero]) # logger.debug('ev_neg = {} from losses {} * -eq {}'.format(ev_neg, losses, (1 - equities[self.hero]))) ev = ev_pos + ev_neg logger.info('Net EV: {} from {} + {}'.format( ev, ev_pos, ev_neg)) result = { 'ev': ev, 'traversed': 1, } # logger.info('{} leaf has result {}'.format(n.tag, result)) n.data.update(result) return # node is all good (not leaf (has children) and not hero folding) # get child actions and process most probable action a_node = self.most_probable_action(n) action = a_node.data['action'] # logger.info('taking next child node action {}'.format(action)) # if it is hero and he folds, # it is not necessarily an immediate ZERO equity # since my previous contrib needs to be added to the pot (i.e. contribs after starting mc) # i.e. make this a leaf node implicitly # no child nodes to remove for fold if action == 'fold' and self.hero == a_node.data['seat']: winnings, losses = self.net(e) result = { 'ev': losses, 'traversed': 1, } # logger.info('hero has folded the child node selected: {}'.format(result)) a_node.data.update(result) # logger.info('a_node data after: {}'.format(a_node.data)) # else we must process the node else: # logger.info('taking action {} and processing that node'.format(action)) cmd = [action[0]] if 'amount' in a_node.data: cmd.append(a_node.data['amount']) # logger.debug('Adding bet value of {}'.format(a_node.data['amount'])) e.do(cmd) self.process_node(e, a_node) # action node has been processed, now update node self.update_node(n) def update_node(self, node): """Update the node's data If leaf, then it was already calculated during processing, and now do not change it: the ev is the ev Minimax applied, hero pick best and foe picks min after p Traversed will stay the traversed_focus level for leaves, but for parent nodes the traversed will be the number of leaves reached from that node. """ is_hero = node.data.get('seat') == self.hero # logger.debug('is hero? {}'.format(is_hero)) # it will traverse back up to the root # root can be skipped if node.is_root(): # input('hero {} node data {}'.format(self.hero, node.data.get('seat'))) # if is_hero: # self.rolling_10.append(abs(self.last_ev)) # self.rolling_40.append(abs(self.last_ev)) # logger.debug('Added {} ev to collection'.format(self.last_ev)) # input('Added {} ev to collection'.format(self.last_ev)) # logger.debug('reached the root') # self.update_ev_change() return # fast forwarding will send here, just ignore node if leaf if node.is_leaf(): # logger.debug('not updating {}: it is final game result (no leaf nodes)'.format(node.tag)) # logger.debug('not updating {}: final data {}'.format(node.tag, node.data)) return depth = self.tree.depth(node) # logger.info('updating node {} at depth {}'.format(node.tag, depth)) # logger.info('node has {} before update'.format(node.data)) if not len(node.fpointer): # logger.error('node {} with {} as no children...'.format(node.tag, node.data)) raise Exception('not necessary to process leaves') # logger.debug('extracting data from {} children nodes...'.format(len(node.fpointer))) n_ev = float('-inf') if is_hero else 0 n_traversed = 0 for child_nid in node.fpointer: child_node = self.tree[child_nid] # logger.debug('child node {} has {}'.format(child_node.tag, child_node.data)) dat = child_node.data if not dat['traversed']: # logger.debug('skipping untraversed {}'.format(child_node.tag)) continue # get max for hero if is_hero: # todo is this +ev dampening necessary # todo this should be fixed when setting for hand range # equities = PE.showdown_equities(self.engine) # n_ev = max(n_ev, dat['ev'] * equities.get(self.hero, 0)) n_ev = max(n_ev, dat['ev']) # get min for foe else: # ev_adj = dat['ev'] * dat['stats'] # logger.debug('foe min between {} and {}'.format(n_ev, ev_adj)) # n_ev = min(n_ev, ev_adj) n_ev += dat['ev'] * dat['stats'] / dat['divider'] n_traversed += dat['traversed'] # logger.debug('added {} traversed: now have {} so far'.format(dat['traversed'], n_traversed)) self.last_ev = node.data['ev'] - n_ev node.data.update({ 'ev': n_ev, 'traversed': n_traversed, }) # logger.info('now node has {} ev~{} after {}'.format(node.tag, round(n_ev, 3), n_traversed)) if not node.data['traversed']: raise Exception('node cannot be untraversed') def net(self, e): """Stored the balance at the start of sim. Now calculate difference as player total matched contrib. Winnings will be less initial starting contrib. """ e.gather_the_money() p = e.players[self.hero] d = e.data[self.hero] matched_diff = d['matched'] - e.matched_start # logger.debug('matched diff = {} from {} - {}'.format(matched_diff, d['matched'], e.matched_start)) winnings = int(e.pot - matched_diff) # logger.debug('winnings diff = {} from pot {} less matched {}'.format(winnings, e.pot, matched_diff)) losses = int(-matched_diff) # logger.info('Winnings = {} and losses = {}'.format(winnings, losses)) return winnings, losses def most_probable_action(self, parent): """All nodes will be processed once at least but it will never happen. Just return the most probable node for most accurate play. Using stats fields on data There should not be any untraversed nodes. So first get untraversed, then sort and pop first one""" # logger.info('getting most probable action after {}'.format(parent.tag)) children = self.tree.children(parent.identifier) children = [c for c in children if not c.data['traversed']] if not children: raise MonteCarloError( 'Cannot choose most probable action when all nodes are traversed' ) children.sort(key=lambda c: c.data['stats'], reverse=True) child = children[0] # logger.debug('{} is untraversed, returning that node for actioning'.format(child.tag)) self.leaf_path.append(child.identifier) return child def add_actions(self, e, parent): """Add actions available to this node If in GG phase then no actions possible, ever. Remove 'hand' Bets: - preflop are 2-4x BB - postflop are 40-100% pot Raise: - always double Allin: - only on river - if out of money then converted to allin Scale non-fold probabilities even though it should not have an effect. """ # logger.info('adding actions to {}'.format(parent.tag)) actions = e.available_actions() s, p = e.q[0] d = e.data[s] balance_left = p['balance'] - d['contrib'] if not actions: # logger.warn('no actions to add to node') return if 'gg' in actions: # logger.debug('no actions available, got gg') return actions.remove('hand') # remove fold if player can check if 'check' in actions: actions.remove('fold') # # logger.debug('removed fold when check available') # remove fold for hero # if s == self.hero and 'fold' in actions: # actions.remove('fold') # # logger.debug('removed fold from hero') # remove raise if player has already been aggressive if 'raise' in actions and any(pa['action'] in 'br' for pa in d[e.phase]): actions.remove('raise') # # logger.debug('removed raise as player has already been aggressive') # remove allin, but add it later with final stats (if increased from bet/raised) if 'allin' in actions: actions.remove('allin') # logger.debug('removed allin by default') # load stats (codes with counts) stats = ES.player_stats(e, s) max_contrib = max(pd['contrib'] for pd in e.data.values()) # contrib_short = max_contrib - d['contrib'] # allin needs to be the doc count # where bets and raises result in allin, add those prob dists to this # that will give proper probability go_allin = stats['actions'].get('a', 0) # # logger.info('filtered actions: {}'.format(actions)) # ev 0 instead of none because of root node sum when not all traversed it gives error action_nodes = [] for a in actions: node_data = { 'stats': stats['actions'].get(ACTIONS_TO_ABBR[a], 0.01), 'divider': 1, 'action': a, 'phase': e.phase, 'seat': s, 'name': p['name'], 'traversed': 0, 'ev': 0, } if a in ['bet', 'raise']: btps_and_amts = [] total_pot = sum(pd['contrib'] for pd in e.data.values()) + e.pot # for preflop only do 2x and 3x if e.phase == e.PHASE_PREFLOP: btps_and_amts.append(('double', e.bb_amt * 2)) btps_and_amts.append(('triple', e.bb_amt * 3)) # else do half and full pots else: btps_and_amts.append(('half_pot', total_pot * 0.50)) btps_and_amts.append(('full_pot', total_pot * 1.00)) # round bets up to a BB # btps_and_amts = [(btp, -(amt // -e.bb_amt) * e.bb_amt) # for btp, amt in btps_and_amts] betting_info = [] amts_seen = [] for btp, amt in btps_and_amts: if amt in amts_seen: # logger.debug('already using {}, skipping duplicate'.format(amt)) continue if a == 'bet' and amt < e.bb_amt: # logger.debug('bet cannot be less than BB {}'.format(e.bb_amt)) continue if a == 'raise' and amt < (max_contrib * 2): # logger.debug('raise cannot be less than 2x contrib of {}'.format(max_contrib * 2)) continue betting_info.append((btp, amt)) amts_seen.append(amt) # change raises that cause allin betting_info_final = [] for btp, amt in betting_info: # if amt is more than player balance, it is an allin if amt >= balance_left: go_allin += node_data['stats'] / len(betting_info) else: betting_info_final.append((btp, amt)) # all good, can have this bet as option for btp, amt in betting_info_final: node_data_copy = deepcopy(node_data) node_data_copy['divider'] = len(betting_info_final) node_data_copy['action'] = f'{a}_{btp}' node_data_copy['amount'] = amt action_nodes.append(node_data_copy) else: action_nodes.append(node_data) # allin will have doc counts (from stat, maybe from bets, maybe from raise) if go_allin: node_data = { 'stats': go_allin, 'divider': 1, 'action': 'allin', 'phase': e.phase, 'seat': s, 'name': p['name'], 'traversed': 0, 'ev': 0, 'amount': balance_left, } action_nodes.append(node_data) # logger.debug('added allin to actions with stat {}'.format(node_data['stats'])) # scale the stats (it is currently term counts aka histogram) and it is required to be # a probability distribution (p~1) # Also, certain actions like fold can be removed, and the total stats is not 1 total_stats = sum(an['stats'] / an['divider'] for an in action_nodes) for action_node in action_nodes: action_node['stats'] = max( 0.01, action_node['stats'] / action_node['divider'] / total_stats) action_node[ 'cum_stats'] = parent.data['cum_stats'] * action_node['stats'] node_tag = f'{action_node["action"]}_{s}_{e.phase}' identifier = f'{node_tag}_{str(uuid.uuid4())[:8]}' self.tree.create_node(identifier=identifier, tag=node_tag, parent=parent.identifier, data=action_node) # logger.debug('new {} for {} with data {}'.format(node_tag, s, action_node)) item = (1 - action_node['cum_stats'], self.leaf_path + [identifier]) self.queue.put(item) # logger.debug('new {} for {} with data {}'.format(node_tag, s, action_node)) # logger.info('{} node actions added'.format(len(action_nodes))) def analyze_tree(self): """Analyze tree to inspect best action from ev""" # self.tree.show() # check all finished paths for path in self.tree.paths_to_leaves(): # skip untraversed end last_node = self.tree[path[-1]] if not last_node.data['traversed']: logger.debug('skipping untraversed endpoint {}'.format( last_node.tag)) continue # show all actions for nid in path: node = self.tree[nid] d = node.data logger.info('Node: {} ev={}'.format(node.tag, d['ev'])) 0 / 0 input('$ check tree') def get_showdown_equities(self, e): """instead of using pokereval, use hs from se""" hss = {} for s, d in e.data.items(): if 'in' in d['status']: hss[s] = ES.showdown_hs(e, s, percentile=self.PERCENTILE) # calculate for hero if self.hero in hss: d = e.data[self.hero] hss[self.hero] = PE.hand_strength(d['hand'], e.board, e.rivals) # normalize total = sum(hs for hs in hss.values()) equities = {s: hs / total for s, hs in hss.items()} return equities
with open("./aoc06.txt") as file: input = list(file.read().splitlines()) # root, first = input[0].split(')') tree = Tree() # tree.create_node(root, root) # root node # tree.create_node(first, first, parent=root) for i in range(0, len(input)): parent, n = input[i].split(')') try: tree.create_node(n, n, parent=parent) except NodeIDAbsentError: tree.create_node(parent, parent) tree.create_node(n, n, parent=parent) paths = tree.paths_to_leaves() result = 0 planets = [] for path in paths: text = '' for i, planet in enumerate(path): text += planet if text not in planets: planets.append(text) result += i print(result)
print() print(x.identifier) print() y = tree.parent("m") print(y.tag) print() print(y.identifier) print() z = tree.get_node("h2") print(z.tag) print() print(z.is_root()) print() print(z.is_leaf()) print() print(tree.paths_to_leaves()) def duplicate_node_path_check(tree, node): check_node = tree.get_node(node) current_node = check_node while not current_node.is_root(): current_node = tree.parent(current_node.identifier) if check_node.tag == current_node.tag: return True return False print(duplicate_node_path_check(tree, 'h2'))