-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaccuracy.py
More file actions
303 lines (255 loc) · 9.89 KB
/
Copy pathaccuracy.py
File metadata and controls
303 lines (255 loc) · 9.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
#---------------------------------------------------------------------
# accuracy.py
# Owen Travis
# For reading SGF files, identifying suspected robots, spawning sub-
# processes running KataGo, and determining the optimal move in each
# position. The core logic of this code is rooted in files written for
# our previous work (see Travis et al. 2023).
#---------------------------------------------------------------------
# Import functions from Sgfmill and Sgfmillplus
from sgfmillplus import get_root, is_go, has_multiple_moves, get_player_names, get_player_ranks
from sgfmillplus import get_time_system, get_overtime_system, get_game_result, playernames_contain_substrings
from sgfmill import common
# Import libraries
import os
import subprocess
import pandas as pd
# Number of jobs (if using a job array on the cluster)
NUMJOBS = 300
# Paths to other files
KATAGO = "/path/to/KataGo/executable"
MODEL = "/path/to/KataGo/model/g170-b30c320x2-s4824661760-d1229536699.bin.gz"
CFG_FILE = "/path/to/gtp/cfg/file"
# List of substrings for identifying robots. Additional robots are later
# flagged in data processing (see: accuracy.rmd).
BOT_PARTIALS = {"kata", "zen", "petgo", "gnugo", "gomancer", "nexus",
"neural", "sgmdb", "alphacent1", "dcnn", "golois", "bot", "tw001", "pachipachi"}
# Class storing data about each move.
# I should change this to a namedtuple.
class MoveInfo:
def __init__(self,
num,
color,
gtp_vertex,
in_overtime,
playerName,
gameFile,
playerRank,
timeSystem,
overtimeSystem,
gameResult):
self.num = num
self.color = color
self.gtp_vertex = gtp_vertex
self.in_overtime = in_overtime
self.playerName = playerName
self.gameFile = gameFile
self.playerRank = playerRank
self.timeSystem = timeSystem
self.overtimeSystem = overtimeSystem
self.gameResult = gameResult
self.played_dx = None
self.played_dy = None
self.analyzed = None
self.bestMove = None
self.prev_gtp_vertex = None
self.best_dx = None
self.best_dy = None
self.isBot = None
def __str__(self):
res = f"{self.num:<3} {self.color:<1}"
res = res + f" {self.gtp_vertex:<4} {self.in_overtime:<2} "
res += f"{self.played_dx} "
res += f"{self.played_dy} "
res += str(self.analyzed) + " "
res += str(self.bestMove) + " "
res += str(self.prev_gtp_vertex) + " "
return res
# Spawn a subprocess running KataGo and feed it kata_input
def runkata(kata_input, cfg_file, output_file):
cmd = []
cmd.append(KATAGO)
cmd.append("gtp")
cmd.append("-model")
cmd.append(MODEL)
cmd.append("-config")
cmd.append(cfg_file)
with subprocess.Popen(cmd, text=True,
stderr=subprocess.PIPE, stdin=subprocess.PIPE, stdout=output_file) as proc:
# wait for setup
while True:
errLine = proc.stderr.readline()
if "GTP ready" in errLine:
break
# play out the game
proc.communicate(kata_input)
# Advance past the handicap stones, which may or may not be recorded
# as moves in the sgf file.
def handle_handicap(root, curr):
if root.has_property("HA") and curr.get_move()[0] == curr[0].get_move()[0]:
# Handicap exists and is played out
print("Handicap is played out")
handicap = root.get("HA")
for _ in range(handicap):
curr = curr[0]
else:
# Handicap is not played out in sgf, or there is no handicap
print("Handicap is not played out")
handicap = 0
return handicap, curr
# Generate KataGo input to be passed to runkata
def get_katago_input(root, filepath, data_folder, allMovesL, whiteIsBot, blackIsBot):
kata_list = []
curr = root[0]
# Advance past the handicap moves
try:
handicap, curr = handle_handicap(root, curr)
except Exception as e:
print(e)
return False
# KataGo input: load game file past the handicap and eliminate time settings.
kata_list.append(" ".join(["loadsgf", os.path.join(data_folder, filepath), str(handicap + 1)]))
kata_list.append("kata-time_settings none")
# Track move count
count = 1
bots = set()
if whiteIsBot:
bots.add('w')
if blackIsBot:
bots.add('b')
player_names = get_player_names(root)
player_ranks = get_player_ranks(root)
time_system = get_time_system(root)
overtime_system = get_overtime_system(root)
game_result = get_game_result(root)
prev_sgf_vertex = None
# Iterate through each move of the game
while True:
color, sgf_vertex = curr.get_move()
gtp_vertex = common.format_vertex(sgf_vertex)
moveO = MoveInfo(count,
color,
gtp_vertex,
in_overtime=curr.has_property("O" + color.upper()),
playerName = player_names[color],
gameFile = filepath,
playerRank = player_ranks[color],
timeSystem = time_system,
overtimeSystem = overtime_system,
gameResult = game_result
)
moveO.prev_gtp_vertex = common.format_vertex(prev_sgf_vertex)
if prev_sgf_vertex and sgf_vertex:
moveO.played_dx = int(abs(sgf_vertex[1]-prev_sgf_vertex[1]))
moveO.played_dy = int(abs(sgf_vertex[0]-prev_sgf_vertex[0]))
moveO.isBot = color in bots
if color in bots or not prev_sgf_vertex:
moveO.analyzed = False
# If this is a human move, analyze the optimal move.
else:
moveO.analyzed = True
kata_list.append("clear_cache")
kata_list.append("kata-genmove_analyze " + color + " maxmoves 3")
kata_list.append("undo")
allMovesL.append(moveO)
kata_list.append(" ".join(["play", color, gtp_vertex]))
# Exit the loop if there are no more moves in the game
if len(curr) == 0:
break
# If the current move was not a "pass", update the previous move
if sgf_vertex:
prev_sgf_vertex = sgf_vertex
# Advance the loop
curr = curr[0]
count += 1
return "\n".join(kata_list) + "\n"
# Process KataGo output and extract the optimal moves
def readOutput(allMovesL, outputF):
index = 0
for line in outputF:
if line.startswith("play"):
while not allMovesL[index].analyzed:
index += 1
moveO = allMovesL[index]
moveO.bestMove = line.split()[-1]
index += 1
# Compute Manhattan distances from each move to the previous move
def addDistancesToMoveO(moveO):
bestMoveSGF = common.move_from_vertex(moveO.bestMove, 19)
prevVertexSGF = common.move_from_vertex(moveO.prev_gtp_vertex, 19)
if bestMoveSGF and prevVertexSGF:
moveO.best_dx = int(abs(bestMoveSGF[1] - prevVertexSGF[1]))
moveO.best_dy = int(abs(bestMoveSGF[0] - prevVertexSGF[0]))
# Validate each game file, generate KataGo input, run KataGo,
# and process KataGo output.
def main_helper(filepath, data_folder, dfs):
allMovesL = []
print(f"Loading file {filepath}.")
# Get the root of the Sgf_game object
try:
root = get_root(os.path.join(data_folder, filepath))
except:
print("Quitting. Not a valid sgf file.")
return
# Check that the game is valid
if not is_go(root):
print("Quitting. Game not identified as Go.")
return
if not has_multiple_moves(root):
print("Quitting. Game has fewer than two moves.")
return
print("Root is valid.")
# Identify robots
bot_status = playernames_contain_substrings(root, BOT_PARTIALS)
if bot_status["b"] and bot_status["w"]:
print("Quitting. Found two bots.")
return
# Generate KataGo input
katago_input = get_katago_input(root, filepath, data_folder, allMovesL, bot_status["w"], bot_status["b"])
if not katago_input:
print("Quitting. Issue generating katago input.")
return
output_filepath = "/file/for/saving/KataGo/output"
# Run KataGo
print("Saving to " + str(output_filepath))
print("Running katago.")
with open(output_filepath, "w+") as outputF:
runkata(katago_input, CFG_FILE, outputF)
print("Saved output to: " + output_filepath)
# Read KataGo output
with open(output_filepath, "r") as outputF:
readOutput(allMovesL, outputF)
# Calculate distances
for moveO in allMovesL:
if moveO.analyzed:
addDistancesToMoveO(moveO)
# Edge case: the first move of the game
allMovesL[0].prev_gtp_vertex = None
dfs.append(pd.DataFrame([vars(s) for s in allMovesL]))
# Given a data folder
def main():
dfs = []
# Set to False if running a non-array cluster job
is_array_job = True
# Set to False if testing on local machine
on_cluster = True
if is_array_job:
job_idx = int(os.environ["SLURM_ARRAY_TASK_ID"]) - 1
else:
job_idx = -1
if on_cluster:
data_folder = '/path/to/data/folder/on/cluster'
else:
data_folder = '/path/to/data/folder/on/local/machine'
with open(os.path.join(data_folder, "gamesList.txt"), "r") as gamesList:
filenames = gamesList.readlines()
# Divide the game files among NUMJOBS jobs
for i in range(len(filenames)):
if i % NUMJOBS == job_idx or job_idx == -1:
main_helper(filenames[i].strip(), data_folder, dfs)
if on_cluster:
pd.concat(dfs, ignore_index=True).to_csv(f'/path/to/output/folder/{job_idx}.csv')
else:
pd.concat(dfs, ignore_index=True).to_csv('/path/to/output/file.csv')
if __name__ == "__main__":
main()