maia2 0.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
maia2/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """An amazing sample package for maia2."""
2
+
3
+ __version__ = "0.1"
maia2/fetch_data.sh ADDED
@@ -0,0 +1,28 @@
1
+ #!/bin/bash
2
+
3
+ # Loop over each year
4
+ for year in {2018..2023}; do
5
+ # Loop over each month
6
+ for month in {05..12}; do
7
+ # Form the URL
8
+ url="https://database.lichess.org/standard/lichess_db_standard_rated_${year}-${month}.pgn.zst"
9
+
10
+ # Check if the URL exists
11
+ if wget --spider $url 2>/dev/null; then
12
+ # Download the file
13
+ wget "$url"
14
+
15
+ # The downloaded file name
16
+ filename="lichess_db_standard_rated_${year}-${month}.pgn.zst"
17
+
18
+ else
19
+ echo "File for ${year}-${month} does not exist."
20
+ fi
21
+
22
+ # Stop the loop if we've reached October 2023
23
+ if [ "$year" -eq 2023 ] && [ "$month" = "10" ]; then
24
+ break
25
+ fi
26
+ done
27
+
28
+ done
maia2/inference.py ADDED
@@ -0,0 +1,154 @@
1
+ from utils import *
2
+ from main import *
3
+
4
+
5
+ class TestDataset(torch.utils.data.Dataset):
6
+
7
+ def __init__(self, data, all_moves_dict, elo_dict):
8
+
9
+ self.all_moves_dict = all_moves_dict
10
+ self.data = data.values.tolist()
11
+ self.elo_dict = elo_dict
12
+
13
+ def __len__(self):
14
+
15
+ return len(self.data)
16
+
17
+ def __getitem__(self, idx):
18
+
19
+ fen, move, elo_self, elo_oppo = self.data[idx]
20
+
21
+ if fen.split(' ')[1] == 'w':
22
+ board = chess.Board(fen)
23
+ elif fen.split(' ')[1] == 'b':
24
+ board = chess.Board(fen).mirror()
25
+ move = mirror_move(move)
26
+ else:
27
+ raise ValueError(f"Invalid fen: {fen}")
28
+
29
+ board_input = board_to_tensor(board)
30
+
31
+ elo_self = map_to_category(elo_self, self.elo_dict)
32
+ elo_oppo = map_to_category(elo_oppo, self.elo_dict)
33
+
34
+ legal_moves, _ = get_side_info(board, move, self.all_moves_dict)
35
+
36
+ return fen, board_input, elo_self, elo_oppo, legal_moves
37
+
38
+
39
+ def get_preds(model, dataloader, all_moves_dict_reversed, cfg_inference):
40
+
41
+ all_probs = []
42
+ predicted_move_probs = []
43
+ predicted_moves = []
44
+ predicted_win_probs = []
45
+
46
+ model.eval()
47
+ with torch.no_grad():
48
+
49
+ for fens, boards, elos_self, elos_oppo, legal_moves in dataloader:
50
+
51
+ if cfg_inference.gpu:
52
+ boards = boards.cuda()
53
+ elos_self = elos_self.cuda()
54
+ elos_oppo = elos_oppo.cuda()
55
+ legal_moves = legal_moves.cuda()
56
+
57
+ logits_maia, _, logits_value = model(boards, elos_self, elos_oppo)
58
+ logits_maia_legal = logits_maia * legal_moves
59
+ probs = logits_maia_legal.softmax(dim=-1)
60
+
61
+ all_probs.append(probs.cpu())
62
+ predicted_move_probs.append(probs.max(dim=-1).values.cpu())
63
+ predicted_move_indices = probs.argmax(dim=-1)
64
+ for i in range(len(fens)):
65
+ fen = fens[i]
66
+ predicted_move = all_moves_dict_reversed[predicted_move_indices[i].item()]
67
+ if fen.split(' ')[1] == 'b':
68
+ predicted_move = mirror_move(predicted_move)
69
+ predicted_moves.append(predicted_move)
70
+
71
+ predicted_win_probs.append((logits_value / 2 + 0.5).cpu())
72
+
73
+ all_probs = torch.cat(all_probs).cpu().numpy()
74
+ predicted_move_probs = torch.cat(predicted_move_probs).numpy()
75
+ predicted_win_probs = torch.cat(predicted_win_probs).numpy()
76
+
77
+ return all_probs, predicted_move_probs, predicted_moves, predicted_win_probs
78
+
79
+
80
+ def inference_batch(data):
81
+
82
+ cfg = parse_args()
83
+ cfg_inference = parse_inference_args()
84
+ if cfg_inference.verbose:
85
+ show_cfg(cfg_inference)
86
+
87
+ all_moves = get_all_possible_moves()
88
+ all_moves_dict = {move: i for i, move in enumerate(all_moves)}
89
+ elo_dict = create_elo_dict()
90
+
91
+ model = MAIA2Model(len(all_moves), elo_dict, cfg)
92
+ model = nn.DataParallel(model)
93
+
94
+ checkpoint = torch.load(cfg_inference.model_path, map_location='cpu')
95
+ model.load_state_dict(checkpoint['model_state_dict'])
96
+ model = model.module
97
+
98
+ if cfg_inference.gpu:
99
+ model = model.cuda()
100
+
101
+ all_moves_dict_reversed = {v: k for k, v in all_moves_dict.items()}
102
+ dataset = TestDataset(data, all_moves_dict, elo_dict)
103
+ dataloader = torch.utils.data.DataLoader(dataset,
104
+ batch_size=cfg_inference.batch_size,
105
+ shuffle=False,
106
+ drop_last=False,
107
+ num_workers=cfg_inference.num_workers)
108
+ if cfg_inference.verbose:
109
+ dataloader = tqdm.tqdm(dataloader)
110
+ all_probs, predicted_move_probs, predicted_moves, predicted_win_probs = get_preds(model, dataloader, all_moves_dict_reversed, cfg_inference)
111
+
112
+ data['predicted_move'] = predicted_moves
113
+ data['predicted_move_prob'] = predicted_move_probs
114
+ data['predicted_win_prob'] = predicted_win_probs
115
+ data['all_probs'] = all_probs.tolist()
116
+
117
+ return data
118
+
119
+ def parse_inference_args(args=None):
120
+
121
+ parser = argparse.ArgumentParser()
122
+
123
+ parser.add_argument('--verbose', default=1, type=int)
124
+ parser.add_argument('--batch_size', default=2048, type=int)
125
+ parser.add_argument('--num_workers', default=16, type=int)
126
+ parser.add_argument('--model_path', default='your_model_path', type=str)
127
+ parser.add_argument('--gpu', default=True, type=bool)
128
+
129
+ return parser.parse_args(args)
130
+
131
+ def show_cfg(cfg):
132
+ print('Configurations:', flush=True)
133
+ for arg in vars(cfg):
134
+ print(f'\t{arg}: {getattr(cfg, arg)}', flush=True)
135
+
136
+
137
+ if __name__ == '__main__':
138
+
139
+ data = pd.read_csv('../data/all_reduced_rapid.csv')
140
+ data = data[data.move_ply > 10]
141
+
142
+ data_novice = data[data['rounded_elo'] <= 1500][['board', 'move', 'active_elo', 'opponent_elo']]
143
+ data_intermediate = data[(data['rounded_elo'] > 1500) & (data['rounded_elo'] < 2000)][['board', 'move', 'active_elo', 'opponent_elo']]
144
+ data_advanced = data[data['rounded_elo'] >= 2000][['board', 'move', 'active_elo', 'opponent_elo']]
145
+ print(f'lens: {len(data_novice)}, {len(data_intermediate)}, {len(data_advanced)}', flush=True)
146
+
147
+ results = []
148
+ for split in [data_novice, data_intermediate, data_advanced]:
149
+ inference_batch(split)
150
+ print(round(len(split[split['predicted_move'] == split['move']]) / len(split), 4))
151
+
152
+
153
+
154
+
maia2/main.py ADDED
@@ -0,0 +1,742 @@
1
+ import chess.pgn
2
+ import chess
3
+ import pdb
4
+ from multiprocessing import Pool, cpu_count, Queue, Process
5
+ import torch
6
+ import tqdm
7
+ import argparse
8
+ from utils import *
9
+ import torch.nn as nn
10
+ import torch.nn.functional as F
11
+ from tqdm.contrib.concurrent import process_map
12
+ import os
13
+ import pandas as pd
14
+ import time
15
+ from einops import rearrange
16
+
17
+
18
+ def process_chunks(cfg, pgn_path, pgn_chunks, elo_dict):
19
+
20
+ # process_per_chunk((pgn_chunks[0][0], pgn_chunks[0][1], pgn_path, elo_dict, cfg))
21
+
22
+ if cfg.verbose:
23
+ results = process_map(process_per_chunk, [(start, end, pgn_path, elo_dict, cfg) for start, end in pgn_chunks], max_workers=len(pgn_chunks), chunksize=1)
24
+ else:
25
+ with Pool(processes=len(pgn_chunks)) as pool:
26
+ results = pool.map(process_per_chunk, [(start, end, pgn_path, elo_dict, cfg) for start, end in pgn_chunks])
27
+
28
+ ret = []
29
+ count = 0
30
+ list_of_dicts = []
31
+ for result, game_count, frequency in results:
32
+ ret.extend(result)
33
+ count += game_count
34
+ list_of_dicts.append(frequency)
35
+
36
+ total_counts = {}
37
+
38
+ for d in list_of_dicts:
39
+ for key, value in d.items():
40
+ total_counts[key] = total_counts.get(key, 0) + value
41
+
42
+ print(total_counts, flush=True)
43
+
44
+ return ret, count, len(pgn_chunks)
45
+
46
+
47
+ def process_per_game(game, white_elo, black_elo, white_win, cfg):
48
+
49
+ ret = []
50
+
51
+ board = game.board()
52
+ moves = list(game.mainline_moves())
53
+
54
+ for i, node in enumerate(game.mainline()):
55
+
56
+ move = moves[i]
57
+
58
+ if i >= cfg.first_n_moves:
59
+
60
+ comment = node.comment
61
+ clock_info = extract_clock_time(comment)
62
+
63
+ if i % 2 == 0:
64
+ board_input = board.fen()
65
+ move_input = move.uci()
66
+ elo_self = white_elo
67
+ elo_oppo = black_elo
68
+ active_win = white_win
69
+
70
+ else:
71
+ board_input = board.mirror().fen()
72
+ move_input = mirror_move(move.uci())
73
+ elo_self = black_elo
74
+ elo_oppo = white_elo
75
+ active_win = - white_win
76
+
77
+ if clock_info > cfg.clock_threshold:
78
+ ret.append((board_input, move_input, elo_self, elo_oppo, active_win))
79
+
80
+ board.push(move)
81
+ if i == cfg.max_ply:
82
+ break
83
+
84
+ return ret
85
+
86
+
87
+ def game_filter(game):
88
+
89
+ white_elo = game.headers.get("WhiteElo", "?")
90
+ black_elo = game.headers.get("BlackElo", "?")
91
+ time_control = game.headers.get("TimeControl", "?")
92
+ result = game.headers.get("Result", "?")
93
+ event = game.headers.get("Event", "?")
94
+
95
+ if white_elo == "?" or black_elo == "?" or time_control == "?" or result == "?" or event == "?":
96
+ return
97
+
98
+ if 'Rated' not in event:
99
+ return
100
+
101
+ if 'Rapid' not in event:
102
+ return
103
+
104
+ for _, node in enumerate(game.mainline()):
105
+ if 'clk' not in node.comment:
106
+ return
107
+
108
+ white_elo = int(white_elo)
109
+ black_elo = int(black_elo)
110
+
111
+ if result == '1-0':
112
+ white_win = 1
113
+ elif result == '0-1':
114
+ white_win = -1
115
+ elif result == '1/2-1/2':
116
+ white_win = 0
117
+ else:
118
+ return
119
+
120
+ return game, white_elo, black_elo, white_win
121
+
122
+
123
+ def process_per_chunk(args):
124
+
125
+ start_pos, end_pos, pgn_path, elo_dict, cfg = args
126
+
127
+ ret = []
128
+ game_count = 0
129
+
130
+ frequency = {}
131
+
132
+ with open(pgn_path, 'r', encoding='utf-8') as pgn_file:
133
+
134
+ pgn_file.seek(start_pos)
135
+
136
+ while pgn_file.tell() < end_pos:
137
+
138
+ game = chess.pgn.read_game(pgn_file)
139
+
140
+ if game is None:
141
+ break
142
+
143
+ filtered_game = game_filter(game)
144
+ if filtered_game:
145
+ game, white_elo, black_elo, white_win = filtered_game
146
+ white_elo = map_to_category(white_elo, elo_dict)
147
+ black_elo = map_to_category(black_elo, elo_dict)
148
+
149
+ if white_elo < black_elo:
150
+ range_1, range_2 = black_elo, white_elo
151
+ else:
152
+ range_1, range_2 = white_elo, black_elo
153
+
154
+ freq = frequency.get((range_1, range_2), 0)
155
+ if freq >= cfg.max_games_per_elo_range:
156
+ continue
157
+
158
+ ret_per_game = process_per_game(game, white_elo, black_elo, white_win, cfg)
159
+ ret.extend(ret_per_game)
160
+ if len(ret_per_game):
161
+
162
+ if (range_1, range_2) in frequency:
163
+ frequency[(range_1, range_2)] += 1
164
+ else:
165
+ frequency[(range_1, range_2)] = 1
166
+
167
+ game_count += 1
168
+
169
+ return ret, game_count, frequency
170
+
171
+
172
+ class MAIA1Dataset(torch.utils.data.Dataset):
173
+
174
+ def __init__(self, data, all_moves_dict, elo_dict, cfg):
175
+
176
+ self.all_moves_dict = all_moves_dict
177
+ self.cfg = cfg
178
+ self.data = data.values.tolist()
179
+ self.elo_dict = elo_dict
180
+
181
+ def __len__(self):
182
+
183
+ return len(self.data)
184
+
185
+ def __getitem__(self, idx):
186
+
187
+ fen, move, elo_self, elo_oppo, white_active = self.data[idx]
188
+
189
+ if white_active:
190
+ board = chess.Board(fen)
191
+ else:
192
+ board = chess.Board(fen).mirror()
193
+ move = mirror_move(move)
194
+
195
+ board_input = board_to_tensor(board)
196
+ move_input = self.all_moves_dict[move]
197
+
198
+ elo_self = map_to_category(elo_self, self.elo_dict)
199
+ elo_oppo = map_to_category(elo_oppo, self.elo_dict)
200
+
201
+ legal_moves, side_info = get_side_info(board, move, self.all_moves_dict)
202
+
203
+ return board_input, move_input, elo_self, elo_oppo, legal_moves, side_info
204
+
205
+
206
+ class MAIA2Dataset(torch.utils.data.Dataset):
207
+
208
+
209
+ def __init__(self, data, all_moves_dict, cfg):
210
+
211
+ self.all_moves_dict = all_moves_dict
212
+ self.data = data
213
+ self.cfg = cfg
214
+
215
+ def __len__(self):
216
+
217
+ return len(self.data)
218
+
219
+ def __getitem__(self, idx):
220
+
221
+ board_input, move_uci, elo_self, elo_oppo, active_win = self.data[idx]
222
+
223
+ board = chess.Board(board_input)
224
+ board_input = board_to_tensor(board)
225
+
226
+ legal_moves, side_info = get_side_info(board, move_uci, self.all_moves_dict)
227
+
228
+ move_input = self.all_moves_dict[move_uci]
229
+
230
+ return board_input, move_input, elo_self, elo_oppo, legal_moves, side_info, active_win
231
+
232
+
233
+ class BasicBlock(torch.nn.Module):
234
+
235
+ def __init__(self, in_planes, planes, stride=1):
236
+ super(BasicBlock, self).__init__()
237
+
238
+ mid_planes = planes
239
+
240
+ self.conv1 = torch.nn.Conv2d(in_planes, mid_planes, kernel_size=3, stride=stride, padding=1, bias=False)
241
+ self.bn1 = torch.nn.BatchNorm2d(mid_planes)
242
+ self.conv2 = torch.nn.Conv2d(mid_planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
243
+ self.bn2 = torch.nn.BatchNorm2d(planes)
244
+ self.dropout = nn.Dropout(p=0.5)
245
+
246
+ def forward(self, x):
247
+
248
+ out = self.conv1(x)
249
+ out = self.bn1(out)
250
+ out = F.relu(out)
251
+ out = self.dropout(out)
252
+
253
+ out = self.conv2(out)
254
+ out = self.bn2(out)
255
+ out += x
256
+ out = F.relu(out)
257
+
258
+ return out
259
+
260
+
261
+ class ChessResNet(torch.nn.Module):
262
+
263
+ def __init__(self, block, cfg):
264
+ super(ChessResNet, self).__init__()
265
+
266
+ self.conv1 = torch.nn.Conv2d(cfg.input_channels, cfg.dim_cnn, kernel_size=3, stride=1, padding=1, bias=False)
267
+ self.bn1 = torch.nn.BatchNorm2d(cfg.dim_cnn)
268
+ self.layers = self._make_layer(block, cfg.dim_cnn, cfg.num_blocks_cnn)
269
+ self.conv_last = torch.nn.Conv2d(cfg.dim_cnn, cfg.vit_length, kernel_size=3, stride=1, padding=1, bias=False)
270
+ self.bn_last = torch.nn.BatchNorm2d(cfg.vit_length)
271
+
272
+ def _make_layer(self, block, planes, num_blocks, stride=1):
273
+
274
+ layers = []
275
+ for _ in range(num_blocks):
276
+ layers.append(block(planes, planes, stride))
277
+
278
+ return torch.nn.Sequential(*layers)
279
+
280
+ def forward(self, x):
281
+
282
+ out = F.relu(self.bn1(self.conv1(x)))
283
+ out = self.layers(out)
284
+ out = self.conv_last(out)
285
+ out = self.bn_last(out)
286
+
287
+ return out
288
+
289
+
290
+ class FeedForward(nn.Module):
291
+ def __init__(self, dim, hidden_dim, dropout = 0.):
292
+ super().__init__()
293
+ self.net = nn.Sequential(
294
+ nn.LayerNorm(dim),
295
+ nn.Linear(dim, hidden_dim),
296
+ nn.GELU(),
297
+ nn.Dropout(dropout),
298
+ nn.Linear(hidden_dim, dim),
299
+ nn.Dropout(dropout)
300
+ )
301
+
302
+ def forward(self, x):
303
+ return self.net(x)
304
+
305
+
306
+ class Attention(nn.Module):
307
+ def __init__(self, dim, heads = 8, dim_head = 64, dropout = 0.):
308
+ super().__init__()
309
+ inner_dim = dim_head * heads
310
+ project_out = not (heads == 1 and dim_head == dim)
311
+
312
+ self.heads = heads
313
+ self.scale = dim_head ** -0.5
314
+
315
+ self.norm = nn.LayerNorm(dim)
316
+
317
+ self.attend = nn.Softmax(dim = -1)
318
+ self.dropout = nn.Dropout(dropout)
319
+
320
+ self.to_qkv = nn.Linear(dim, inner_dim * 3, bias = False)
321
+
322
+ self.to_out = nn.Sequential(
323
+ nn.Linear(inner_dim, dim),
324
+ nn.Dropout(dropout)
325
+ ) if project_out else nn.Identity()
326
+
327
+ def forward(self, x):
328
+ x = self.norm(x)
329
+
330
+ qkv = self.to_qkv(x).chunk(3, dim = -1)
331
+ q, k, v = map(lambda t: rearrange(t, 'b n (h d) -> b h n d', h = self.heads), qkv)
332
+
333
+ dots = torch.matmul(q, k.transpose(-1, -2)) * self.scale
334
+
335
+ attn = self.attend(dots)
336
+ attn = self.dropout(attn)
337
+
338
+ out = torch.matmul(attn, v)
339
+ out = rearrange(out, 'b h n d -> b n (h d)')
340
+ return self.to_out(out)
341
+
342
+
343
+ class EloAwareAttention(nn.Module):
344
+ def __init__(self, dim, heads=8, dim_head=64, dropout=0., elo_dim=64):
345
+ super().__init__()
346
+ inner_dim = dim_head * heads
347
+ project_out = not (heads == 1 and dim_head == dim)
348
+
349
+ self.heads = heads
350
+ self.scale = dim_head ** -0.5
351
+
352
+ self.norm = nn.LayerNorm(dim)
353
+
354
+ self.attend = nn.Softmax(dim=-1)
355
+ self.dropout = nn.Dropout(dropout)
356
+
357
+ self.to_qkv = nn.Linear(dim, inner_dim * 3, bias=False)
358
+
359
+ self.elo_query = nn.Linear(elo_dim, inner_dim, bias=False)
360
+
361
+ self.to_out = nn.Sequential(
362
+ nn.Linear(inner_dim, dim),
363
+ nn.Dropout(dropout)
364
+ ) if project_out else nn.Identity()
365
+
366
+ def forward(self, x, elo_emb):
367
+ x = self.norm(x)
368
+
369
+ qkv = self.to_qkv(x).chunk(3, dim=-1)
370
+ q, k, v = map(lambda t: rearrange(t, 'b n (h d) -> b h n d', h=self.heads), qkv)
371
+
372
+ elo_effect = self.elo_query(elo_emb).view(x.size(0), self.heads, 1, -1)
373
+ q = q + elo_effect
374
+
375
+ dots = torch.matmul(q, k.transpose(-1, -2)) * self.scale
376
+
377
+ attn = self.attend(dots)
378
+ attn = self.dropout(attn)
379
+
380
+ out = torch.matmul(attn, v)
381
+ out = rearrange(out, 'b h n d -> b n (h d)')
382
+ return self.to_out(out)
383
+
384
+
385
+ class Transformer(nn.Module):
386
+ def __init__(self, dim, depth, heads, dim_head, mlp_dim, dropout = 0., elo_dim=64):
387
+ super().__init__()
388
+ self.norm = nn.LayerNorm(dim)
389
+ self.layers = nn.ModuleList([])
390
+ self.elo_layers = nn.ModuleList([])
391
+ for _ in range(depth):
392
+ self.elo_layers.append(nn.ModuleList([
393
+ EloAwareAttention(dim, heads = heads, dim_head = dim_head, dropout = dropout, elo_dim = elo_dim),
394
+ FeedForward(dim, mlp_dim, dropout = dropout)
395
+ ]))
396
+
397
+ def forward(self, x, elo_emb):
398
+ for attn, ff in self.elo_layers:
399
+ x = attn(x, elo_emb) + x
400
+ x = ff(x) + x
401
+
402
+ return self.norm(x)
403
+
404
+
405
+ class MAIA2Model(torch.nn.Module):
406
+
407
+ def __init__(self, output_dim, elo_dict, cfg):
408
+ super(MAIA2Model, self).__init__()
409
+
410
+ self.cfg = cfg
411
+ self.chess_cnn = ChessResNet(BasicBlock, cfg)
412
+
413
+ heads = 16
414
+ dim_head = 64
415
+ self.to_patch_embedding = nn.Sequential(
416
+ nn.Linear(8 * 8, cfg.dim_vit),
417
+ nn.LayerNorm(cfg.dim_vit),
418
+ )
419
+ self.transformer = Transformer(cfg.dim_vit, cfg.num_blocks_vit, heads, dim_head, mlp_dim=cfg.dim_vit, dropout = 0.1, elo_dim = cfg.elo_dim * 2)
420
+ self.pos_embedding = nn.Parameter(torch.randn(1, cfg.vit_length, cfg.dim_vit))
421
+
422
+ self.fc_1 = nn.Linear(cfg.dim_vit, output_dim)
423
+ # self.fc_1_1 = nn.Linear(cfg.dim_vit, cfg.dim_vit)
424
+ self.fc_2 = nn.Linear(cfg.dim_vit, output_dim + 6 + 6 + 1 + 64 + 64)
425
+ # self.fc_2_1 = nn.Linear(cfg.dim_vit, cfg.dim_vit)
426
+ self.fc_3 = nn.Linear(128, 1)
427
+ self.fc_3_1 = nn.Linear(cfg.dim_vit, 128)
428
+
429
+ self.elo_embedding = torch.nn.Embedding(len(elo_dict), cfg.elo_dim)
430
+
431
+ self.dropout = nn.Dropout(p=0.1)
432
+ self.last_ln = nn.LayerNorm(cfg.dim_vit)
433
+
434
+
435
+ def forward(self, boards, elos_self, elos_oppo):
436
+
437
+ batch_size = boards.size(0)
438
+ boards = boards.view(batch_size, self.cfg.input_channels, 8, 8)
439
+ embs = self.chess_cnn(boards)
440
+ embs = embs.view(batch_size, embs.size(1), 8 * 8)
441
+ x = self.to_patch_embedding(embs)
442
+ x += self.pos_embedding
443
+ x = self.dropout(x)
444
+
445
+ elos_emb_self = self.elo_embedding(elos_self)
446
+ elos_emb_oppo = self.elo_embedding(elos_oppo)
447
+ elos_emb = torch.cat((elos_emb_self, elos_emb_oppo), dim=1)
448
+ x = self.transformer(x, elos_emb).mean(dim=1)
449
+
450
+ x = self.last_ln(x)
451
+
452
+ logits_maia = self.fc_1(x)
453
+ logits_side_info = self.fc_2(x)
454
+ logits_value = self.fc_3(self.dropout(torch.relu(self.fc_3_1(x)))).squeeze(dim=-1)
455
+
456
+ return logits_maia, logits_side_info, logits_value
457
+
458
+
459
+ def read_monthly_data_path(cfg):
460
+
461
+ print('Training Data:', flush=True)
462
+ pgn_paths = []
463
+
464
+ for year in range(cfg.start_year, cfg.end_year + 1):
465
+ start_month = cfg.start_month if year == cfg.start_year else 1
466
+ end_month = cfg.end_month if year == cfg.end_year else 12
467
+
468
+ for month in range(start_month, end_month + 1):
469
+ formatted_month = f"{month:02d}"
470
+ pgn_path = cfg.data_root + f"/lichess_db_standard_rated_{year}-{formatted_month}.pgn"
471
+ # skip 2019-12
472
+ if year == 2019 and month == 12:
473
+ continue
474
+ print(pgn_path, flush=True)
475
+ pgn_paths.append(pgn_path)
476
+
477
+ return pgn_paths
478
+
479
+
480
+ def evaluate(model, dataloader):
481
+
482
+ counter = 0
483
+ correct_move = 0
484
+
485
+ model.eval()
486
+ with torch.no_grad():
487
+
488
+ for boards, labels, elos_self, elos_oppo, legal_moves, side_info in dataloader:
489
+
490
+ boards = boards.cuda()
491
+ labels = labels.cuda()
492
+ elos_self = elos_self.cuda()
493
+ elos_oppo = elos_oppo.cuda()
494
+ legal_moves = legal_moves.cuda()
495
+
496
+ logits_maia, logits_side_info, logits_value = model(boards, elos_self, elos_oppo)
497
+ logits_maia_legal = logits_maia * legal_moves
498
+ preds = logits_maia_legal.argmax(dim=-1)
499
+ correct_move += (preds == labels).sum().item()
500
+
501
+ counter += len(labels)
502
+
503
+ return correct_move, counter
504
+
505
+
506
+ def evaluate_MAIA1_data(model, all_moves_dict, elo_dict, cfg, tiny=False):
507
+
508
+ elo_list = range(1000, 2600, 100)
509
+
510
+ for i in elo_list:
511
+ start = i
512
+ end = i + 100
513
+ file_path = f"../data/test/KDDTest_{start}-{end}.csv"
514
+ data = pd.read_csv(file_path)
515
+ data = data[data.type == 'Rapid'][['board', 'move', 'active_elo', 'opponent_elo', 'white_active']]
516
+ dataset = MAIA1Dataset(data, all_moves_dict, elo_dict, cfg)
517
+ dataloader = torch.utils.data.DataLoader(dataset,
518
+ batch_size=cfg.batch_size,
519
+ shuffle=False,
520
+ drop_last=False,
521
+ num_workers=cfg.num_workers)
522
+ if cfg.verbose:
523
+ dataloader = tqdm.tqdm(dataloader)
524
+ print(f'Testing Elo Range {start}-{end} with MAIA 1 data:', flush=True)
525
+ correct_move, counter = evaluate(model, dataloader)
526
+ print(f'Accuracy Move Prediction: {round(correct_move / counter, 4)}', flush=True)
527
+ if tiny:
528
+ break
529
+
530
+
531
+ def train_chunks(cfg, data, model, optimizer, all_moves_dict, criterion_maia, criterion_side_info, criterion_value):
532
+
533
+ dataset_train = MAIA2Dataset(data, all_moves_dict, cfg)
534
+ dataloader_train = torch.utils.data.DataLoader(dataset_train,
535
+ batch_size=cfg.batch_size,
536
+ shuffle=True,
537
+ drop_last=False,
538
+ num_workers=cfg.num_workers)
539
+ if cfg.verbose:
540
+ dataloader_train = tqdm.tqdm(dataloader_train)
541
+
542
+ avg_loss = 0
543
+ avg_loss_maia = 0
544
+ avg_loss_side_info = 0
545
+ avg_loss_value = 0
546
+ step = 0
547
+ for boards, labels, elos_self, elos_oppo, legal_moves, side_info, wdl in dataloader_train:
548
+
549
+ model.train()
550
+ boards = boards.cuda()
551
+ labels = labels.cuda()
552
+ elos_self = elos_self.cuda()
553
+ elos_oppo = elos_oppo.cuda()
554
+ side_info = side_info.cuda()
555
+ wdl = wdl.float().cuda()
556
+
557
+ logits_maia, logits_side_info, logits_value = model(boards, elos_self, elos_oppo)
558
+
559
+ loss = 0
560
+ loss_maia = criterion_maia(logits_maia, labels)
561
+ loss += loss_maia
562
+
563
+ if cfg.side_info:
564
+
565
+ loss_side_info = criterion_side_info(logits_side_info, side_info) * cfg.side_info_coefficient
566
+ loss += loss_side_info
567
+
568
+ if cfg.value:
569
+ loss_value = criterion_value(logits_value, wdl) * cfg.value_coefficient
570
+ loss += loss_value
571
+
572
+ optimizer.zero_grad()
573
+ loss.backward()
574
+ optimizer.step()
575
+
576
+ avg_loss += loss.item()
577
+ avg_loss_maia += loss_maia.item()
578
+ if cfg.side_info:
579
+ avg_loss_side_info += loss_side_info.item()
580
+ if cfg.value:
581
+ avg_loss_value += loss_value.item()
582
+ step += 1
583
+
584
+ return round(avg_loss / step, 3), round(avg_loss_maia / step, 3), round(avg_loss_side_info / step, 3), round(avg_loss_value / step, 3)
585
+
586
+
587
+ def preprocess_thread(queue, cfg, pgn_path, pgn_chunks_sublist, elo_dict):
588
+
589
+ data, game_count, chunk_count = process_chunks(cfg, pgn_path, pgn_chunks_sublist, elo_dict)
590
+ queue.put([data, game_count, chunk_count])
591
+ del data
592
+
593
+
594
+ def worker_wrapper(semaphore, *args, **kwargs):
595
+ with semaphore:
596
+ preprocess_thread(*args, **kwargs)
597
+
598
+
599
+ def parse_args(args=None):
600
+
601
+ parser = argparse.ArgumentParser()
602
+
603
+ # Supporting Arguments
604
+ parser.add_argument('--data_root', default='your_data_root', type=str)
605
+ parser.add_argument('--seed', default=42, type=int)
606
+ parser.add_argument('--num_workers', default=16, type=int)
607
+ parser.add_argument('--verbose', default=1, type=int)
608
+ parser.add_argument('--max_epochs', default=3, type=int)
609
+ parser.add_argument('--max_ply', default=300, type=int)
610
+ parser.add_argument('--clock_threshold', default=30, type=int)
611
+ parser.add_argument('--chunk_size', default=20000, type=str)
612
+ parser.add_argument('--start_year', default=2018, type=int)
613
+ parser.add_argument('--start_month', default=5, type=int)
614
+ parser.add_argument('--end_year', default=2023, type=int)
615
+ parser.add_argument('--end_month', default=11, type=int)
616
+ parser.add_argument('--from_checkpoint', default=False, type=bool)
617
+ parser.add_argument('--checkpoint_epoch', default=2, type=int)
618
+ parser.add_argument('--checkpoint_year', default=2018, type=int)
619
+ parser.add_argument('--checkpoint_month', default=6, type=int)
620
+ parser.add_argument('--num_cpu_left', default=16, type=int)
621
+ parser.add_argument('--queue_length', default=2, type=int)
622
+ parser.add_argument('--max_games_per_elo_range', default=20, type=int)
623
+
624
+ # Tunable Arguments
625
+ parser.add_argument('--lr', default=1e-4, type=float)
626
+ parser.add_argument('--wd', default=1e-5, type=float)
627
+ parser.add_argument('--batch_size', default=8192, type=int)
628
+ parser.add_argument('--first_n_moves', default=10, type=int)
629
+ parser.add_argument('--last_n_moves', default=10, type=int)
630
+ parser.add_argument('--dim_cnn', default=256, type=int)
631
+ parser.add_argument('--dim_vit', default=1024, type=int)
632
+ parser.add_argument('--num_blocks_cnn', default=5, type=int)
633
+ parser.add_argument('--num_blocks_vit', default=2, type=int)
634
+ parser.add_argument('--input_channels', default=18, type=int)
635
+ parser.add_argument('--vit_length', default=8, type=int)
636
+ parser.add_argument('--elo_dim', default=128, type=int)
637
+ parser.add_argument('--side_info', default=True, type=bool)
638
+ parser.add_argument('--side_info_coefficient', default=1, type=float)
639
+ parser.add_argument('--value', default=True, type=bool)
640
+ parser.add_argument('--value_coefficient', default=1, type=float)
641
+
642
+ return parser.parse_args(args)
643
+
644
+
645
+ if __name__ == '__main__':
646
+
647
+ cfg = parse_args()
648
+ print('Configurations:', flush=True)
649
+ for arg in vars(cfg):
650
+ print(f'\t{arg}: {getattr(cfg, arg)}', flush=True)
651
+ seed_everything(cfg.seed)
652
+ num_processes = cpu_count() - cfg.num_cpu_left
653
+
654
+ save_root = f'../tmp/{cfg.lr}_{cfg.batch_size}_{cfg.wd}_MAIA2/'
655
+ if not os.path.exists(save_root):
656
+ os.makedirs(save_root)
657
+
658
+ all_moves = get_all_possible_moves()
659
+ all_moves_dict = {move: i for i, move in enumerate(all_moves)}
660
+ elo_dict = create_elo_dict()
661
+
662
+ model = MAIA2Model(len(all_moves), elo_dict, cfg)
663
+
664
+ print(model, flush=True)
665
+ model = model.cuda()
666
+ model = nn.DataParallel(model)
667
+ criterion_maia = nn.CrossEntropyLoss()
668
+ criterion_side_info = nn.BCEWithLogitsLoss()
669
+ criterion_value = nn.MSELoss()
670
+
671
+ optimizer = torch.optim.AdamW(model.parameters(), lr=cfg.lr, weight_decay=cfg.wd)
672
+ N_params = count_parameters(model)
673
+ print(f'Trainable Parameters: {N_params}', flush=True)
674
+
675
+ accumulated_samples = 0
676
+ accumulated_games = 0
677
+
678
+ if cfg.from_checkpoint:
679
+ formatted_month = f"{cfg.checkpoint_month:02d}"
680
+ checkpoint = torch.load(save_root + f'epoch_{cfg.checkpoint_epoch}_{cfg.checkpoint_year}-{formatted_month}.pgn.pt')
681
+ model.load_state_dict(checkpoint['model_state_dict'])
682
+ optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
683
+ accumulated_samples = checkpoint['accumulated_samples']
684
+ accumulated_games = checkpoint['accumulated_games']
685
+
686
+ for epoch in range(cfg.max_epochs):
687
+
688
+ print(f'Epoch {epoch + 1}', flush=True)
689
+ pgn_paths = read_monthly_data_path(cfg)
690
+
691
+ num_file = 0
692
+ for pgn_path in pgn_paths:
693
+
694
+ start_time = time.time()
695
+ decompress_zst(pgn_path + '.zst', pgn_path)
696
+ print(f'Decompressing {pgn_path} took {readable_time(time.time() - start_time)}', flush=True)
697
+
698
+ pgn_chunks = read_or_create_chunks(pgn_path, cfg)
699
+ print(f'Training {pgn_path} with {len(pgn_chunks)} chunks.', flush=True)
700
+
701
+ queue = Queue(maxsize=cfg.queue_length)
702
+
703
+ pgn_chunks_sublists = []
704
+ for i in range(0, len(pgn_chunks), num_processes):
705
+ pgn_chunks_sublists.append(pgn_chunks[i:i + num_processes])
706
+
707
+ pgn_chunks_sublist = pgn_chunks_sublists[0]
708
+ # For debugging only
709
+ # process_chunks(cfg, pgn_path, pgn_chunks_sublist, elo_dict)
710
+ worker = Process(target=preprocess_thread, args=(queue, cfg, pgn_path, pgn_chunks_sublist, elo_dict))
711
+ worker.start()
712
+
713
+ num_chunk = 0
714
+ offset = 0
715
+ while True:
716
+ if not queue.empty():
717
+ if offset + 1 < len(pgn_chunks_sublists):
718
+ pgn_chunks_sublist = pgn_chunks_sublists[offset + 1]
719
+ worker = Process(target=preprocess_thread, args=(queue, cfg, pgn_path, pgn_chunks_sublist, elo_dict))
720
+ worker.start()
721
+ offset += 1
722
+ data, game_count, chunk_count = queue.get()
723
+ loss, loss_maia, loss_side_info, loss_value = train_chunks(cfg, data, model, optimizer, all_moves_dict, criterion_maia, criterion_side_info, criterion_value)
724
+ num_chunk += chunk_count
725
+ accumulated_samples += len(data)
726
+ accumulated_games += game_count
727
+ print(f'[{num_chunk}/{len(pgn_chunks)}]', flush=True)
728
+ print(f'[# Positions]: {readable_num(accumulated_samples)}', flush=True)
729
+ print(f'[# Games]: {readable_num(accumulated_games)}', flush=True)
730
+ print(f'[# Loss]: {loss} | [# Loss MAIA]: {loss_maia} | [# Loss Side Info]: {loss_side_info} | [# Loss Value]: {loss_value}', flush=True)
731
+ if num_chunk == len(pgn_chunks):
732
+ break
733
+
734
+ num_file += 1
735
+ print(f'### ({num_file} / {len(pgn_paths)}) Took {readable_time(time.time() - start_time)} to train {pgn_path} with {len(pgn_chunks)} chunks.', flush=True)
736
+ os.remove(pgn_path)
737
+
738
+ # evaluate_MAIA1_data(model, all_moves_dict, elo_dict, cfg, tiny=False)
739
+ torch.save({'model_state_dict': model.state_dict(),
740
+ 'optimizer_state_dict': optimizer.state_dict(),
741
+ 'accumulated_samples': accumulated_samples,
742
+ 'accumulated_games': accumulated_games}, f'{save_root}epoch_{epoch + 1}_{pgn_path[-11:]}.pt')
maia2/utils.py ADDED
@@ -0,0 +1,347 @@
1
+ import pdb
2
+ import chess
3
+ import pickle
4
+ import os
5
+ import random
6
+ import numpy as np
7
+ import torch
8
+ import time
9
+ import requests
10
+ import tqdm
11
+ import pyzstd
12
+ import re
13
+
14
+
15
+ def seed_everything(seed: int):
16
+
17
+ random.seed(seed)
18
+ os.environ['PYTHONHASHSEED'] = str(seed)
19
+ np.random.seed(seed)
20
+ torch.manual_seed(seed)
21
+ torch.cuda.manual_seed(seed)
22
+ torch.cuda.manual_seed_all(seed)
23
+ torch.backends.cudnn.deterministic = True
24
+ torch.backends.cudnn.benchmark = False
25
+
26
+
27
+ def delete_file(filename):
28
+
29
+ if os.path.exists(filename):
30
+ os.remove(filename)
31
+ print(f"Data {filename} has been deleted.")
32
+ else:
33
+ print(f"The file '{filename}' does not exist.")
34
+
35
+
36
+ def readable_num(num):
37
+
38
+ if num >= 1e9: # if parameters are in the billions
39
+ return f'{num / 1e9:.2f}B'
40
+ elif num >= 1e6: # if parameters are in the millions
41
+ return f'{num / 1e6:.2f}M'
42
+ elif num >= 1e3: # if parameters are in the thousands
43
+ return f'{num / 1e3:.2f}K'
44
+ else:
45
+ return str(num)
46
+
47
+
48
+ def readable_time(elapsed_time):
49
+
50
+ hours, rem = divmod(elapsed_time, 3600)
51
+ minutes, seconds = divmod(rem, 60)
52
+
53
+ if hours > 0:
54
+ return f"{int(hours)}h {int(minutes)}m {seconds:.2f}s"
55
+ elif minutes > 0:
56
+ return f"{int(minutes)}m {seconds:.2f}s"
57
+ else:
58
+ return f"{seconds:.2f}s"
59
+
60
+
61
+ def count_parameters(model):
62
+
63
+ total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
64
+
65
+ return readable_num(total_params)
66
+
67
+
68
+ def create_elo_dict():
69
+
70
+ inteval = 100
71
+ start = 1100
72
+ end = 2000
73
+
74
+ range_dict = {f"<{start}": 0}
75
+ range_index = 1
76
+
77
+ for lower_bound in range(start, end - 1, inteval):
78
+ upper_bound = lower_bound + inteval
79
+ range_dict[f"{lower_bound}-{upper_bound - 1}"] = range_index
80
+ range_index += 1
81
+
82
+ range_dict[f">={end}"] = range_index
83
+
84
+ # print(range_dict, flush=True)
85
+
86
+ return range_dict
87
+
88
+
89
+ def map_to_category(elo, elo_dict):
90
+
91
+ inteval = 100
92
+ start = 1100
93
+ end = 2000
94
+
95
+ if elo < start:
96
+ return elo_dict[f"<{start}"]
97
+ elif elo >= end:
98
+ return elo_dict[f">={end}"]
99
+ else:
100
+ for lower_bound in range(start, end - 1, inteval):
101
+ upper_bound = lower_bound + inteval
102
+ if lower_bound <= elo < upper_bound:
103
+ return elo_dict[f"{lower_bound}-{upper_bound - 1}"]
104
+
105
+
106
+ def get_side_info(board, move_uci, all_moves_dict):
107
+ move = chess.Move.from_uci(move_uci)
108
+
109
+ moving_piece = board.piece_at(move.from_square)
110
+ captured_piece = board.piece_at(move.to_square)
111
+
112
+ from_square_encoded = torch.zeros(64)
113
+ from_square_encoded[move.from_square] = 1
114
+
115
+ to_square_encoded = torch.zeros(64)
116
+ to_square_encoded[move.to_square] = 1
117
+
118
+ if move_uci == 'e1g1':
119
+ rook_move = chess.Move.from_uci('h1f1')
120
+ from_square_encoded[rook_move.from_square] = 1
121
+ to_square_encoded[rook_move.to_square] = 1
122
+
123
+ if move_uci == 'e1c1':
124
+ rook_move = chess.Move.from_uci('a1d1')
125
+ from_square_encoded[rook_move.from_square] = 1
126
+ to_square_encoded[rook_move.to_square] = 1
127
+
128
+ board.push(move)
129
+ is_check = board.is_check()
130
+ board.pop()
131
+
132
+ # Order: Pawn, Knight, Bishop, Rook, Queen, King
133
+ side_info = torch.zeros(6 + 6 + 1)
134
+ side_info[moving_piece.piece_type - 1] = 1
135
+ if move_uci in ['e1g1', 'e1c1']:
136
+ side_info[3] = 1
137
+ if captured_piece:
138
+ side_info[6 + captured_piece.piece_type - 1] = 1
139
+ if is_check:
140
+ side_info[-1] = 1
141
+
142
+ legal_moves = torch.zeros(len(all_moves_dict))
143
+ legal_moves_idx = torch.tensor([all_moves_dict[move.uci()] for move in board.legal_moves])
144
+ legal_moves[legal_moves_idx] = 1
145
+
146
+ side_info = torch.cat([side_info, from_square_encoded, to_square_encoded, legal_moves], dim=0)
147
+
148
+ return legal_moves, side_info
149
+
150
+
151
+ def extract_clock_time(comment):
152
+
153
+ match = re.search(r'\[%clk (\d+):(\d+):(\d+)\]', comment)
154
+ if match:
155
+ hours, minutes, seconds = map(int, match.groups())
156
+ return hours * 3600 + minutes * 60 + seconds
157
+ return None
158
+
159
+
160
+ def read_or_create_chunks(pgn_path, cfg):
161
+
162
+ cache_file = pgn_path.replace('.pgn', '_chunks.pkl')
163
+
164
+ if os.path.exists(cache_file):
165
+ print(f"Loading cached chunks from {cache_file}")
166
+ with open(cache_file, 'rb') as f:
167
+ pgn_chunks = pickle.load(f)
168
+ else:
169
+ print(f"Cache not found. Creating chunks for {pgn_path}")
170
+ start_time = time.time()
171
+ pgn_chunks = get_chunks(pgn_path, cfg.chunk_size)
172
+ print(f'Chunking took {readable_time(time.time() - start_time)}', flush=True)
173
+
174
+ with open(cache_file, 'wb') as f:
175
+ pickle.dump(pgn_chunks, f)
176
+
177
+ return pgn_chunks
178
+
179
+
180
+ def board_to_tensor(board):
181
+
182
+ piece_types = [chess.PAWN, chess.KNIGHT, chess.BISHOP, chess.ROOK, chess.QUEEN, chess.KING]
183
+ num_piece_channels = 12 # 6 piece types * 2 colors
184
+ additional_channels = 6 # 1 for player's turn, 4 for castling rights, 1 for en passant
185
+ tensor = torch.zeros((num_piece_channels + additional_channels, 8, 8), dtype=torch.float32)
186
+
187
+ # Precompute indices for each piece type
188
+ piece_indices = {piece: i for i, piece in enumerate(piece_types)}
189
+
190
+ # Fill tensor for each piece type
191
+ for piece_type in piece_types:
192
+ for color in [True, False]: # True is White, False is Black
193
+ piece_map = board.pieces(piece_type, color)
194
+ index = piece_indices[piece_type] + (0 if color else 6)
195
+ for square in piece_map:
196
+ row, col = divmod(square, 8)
197
+ tensor[index, row, col] = 1.0
198
+
199
+ # Player's turn channel (White = 1, Black = 0)
200
+ turn_channel = num_piece_channels
201
+ if board.turn == chess.WHITE:
202
+ tensor[turn_channel, :, :] = 1.0
203
+
204
+ # Castling rights channels
205
+ castling_rights = [board.has_kingside_castling_rights(chess.WHITE),
206
+ board.has_queenside_castling_rights(chess.WHITE),
207
+ board.has_kingside_castling_rights(chess.BLACK),
208
+ board.has_queenside_castling_rights(chess.BLACK)]
209
+ for i, has_right in enumerate(castling_rights):
210
+ if has_right:
211
+ tensor[num_piece_channels + 1 + i, :, :] = 1.0
212
+
213
+ # En passant target channel
214
+ ep_channel = num_piece_channels + 5
215
+ if board.ep_square is not None:
216
+ row, col = divmod(board.ep_square, 8)
217
+ tensor[ep_channel, row, col] = 1.0
218
+
219
+ return tensor
220
+
221
+ def generate_pawn_promotions():
222
+ # Define the promotion rows for both colors and the promotion pieces
223
+ # promotion_rows = {'white': '7', 'black': '2'}
224
+ promotion_rows = {'white': '7'}
225
+ promotion_pieces = ['q', 'r', 'b', 'n']
226
+ promotions = []
227
+
228
+ # Iterate over each color
229
+ for color, row in promotion_rows.items():
230
+ # Target rows for promotion (8 for white, 1 for black)
231
+ target_row = '8' if color == 'white' else '1'
232
+
233
+ # Each file from 'a' to 'h'
234
+ for file in 'abcdefgh':
235
+ # Direct move to promotion
236
+ for piece in promotion_pieces:
237
+ promotions.append(f'{file}{row}{file}{target_row}{piece}')
238
+
239
+ # Capturing moves to the left and right (if not on the edges of the board)
240
+ if file != 'a':
241
+ left_file = chr(ord(file) - 1) # File to the left
242
+ for piece in promotion_pieces:
243
+ promotions.append(f'{file}{row}{left_file}{target_row}{piece}')
244
+
245
+ if file != 'h':
246
+ right_file = chr(ord(file) + 1) # File to the right
247
+ for piece in promotion_pieces:
248
+ promotions.append(f'{file}{row}{right_file}{target_row}{piece}')
249
+
250
+ return promotions
251
+
252
+
253
+ def mirror_square(square):
254
+
255
+ file = square[0]
256
+ rank = str(9 - int(square[1]))
257
+
258
+ return file + rank
259
+
260
+
261
+ def mirror_move(move_uci):
262
+ # Check if the move is a promotion (length of UCI string will be more than 4)
263
+ is_promotion = len(move_uci) > 4
264
+
265
+ # Extract the start and end squares, and the promotion piece if applicable
266
+ start_square = move_uci[:2]
267
+ end_square = move_uci[2:4]
268
+ promotion_piece = move_uci[4:] if is_promotion else ""
269
+
270
+ # Mirror the start and end squares
271
+ mirrored_start = mirror_square(start_square)
272
+ mirrored_end = mirror_square(end_square)
273
+
274
+ # Return the mirrored move, including the promotion piece if applicable
275
+ return mirrored_start + mirrored_end + promotion_piece
276
+
277
+
278
+ def get_chunks(pgn_path, chunk_size):
279
+
280
+ chunks = []
281
+ with open(pgn_path, 'r', encoding='utf-8') as pgn_file:
282
+ while True:
283
+ start_pos = pgn_file.tell()
284
+ game_count = 0
285
+ while game_count < chunk_size:
286
+ line = pgn_file.readline()
287
+ if not line:
288
+ break
289
+ if line[-4:] == "1-0\n" or line[-4:] == "0-1\n":
290
+ game_count += 1
291
+ if line[-8:] == "1/2-1/2\n":
292
+ game_count += 1
293
+ if line[-2:] == "*\n":
294
+ game_count += 1
295
+ line = pgn_file.readline()
296
+ if line not in ["\n", ""]:
297
+ raise ValueError
298
+ end_pos = pgn_file.tell()
299
+ chunks.append((start_pos, end_pos))
300
+ if not line:
301
+ break
302
+
303
+ return chunks
304
+
305
+
306
+ def decompress_zst(file_path, decompressed_path):
307
+ """ Decompress a .zst file using pyzstd """
308
+ with open(file_path, 'rb') as compressed_file, open(decompressed_path, 'wb') as decompressed_file:
309
+ pyzstd.decompress_stream(compressed_file, decompressed_file)
310
+
311
+
312
+ def get_all_possible_moves():
313
+
314
+ all_moves = []
315
+
316
+ for rank in range(8):
317
+ for file in range(8):
318
+ square = chess.square(file, rank)
319
+
320
+ board = chess.Board(None)
321
+ board.set_piece_at(square, chess.Piece(chess.QUEEN, chess.WHITE))
322
+ legal_moves = list(board.legal_moves)
323
+ all_moves.extend(legal_moves)
324
+
325
+ board = chess.Board(None)
326
+ board.set_piece_at(square, chess.Piece(chess.KNIGHT, chess.WHITE))
327
+ legal_moves = list(board.legal_moves)
328
+ all_moves.extend(legal_moves)
329
+
330
+ all_moves = [all_moves[i].uci() for i in range(len(all_moves))]
331
+
332
+ pawn_promotions = generate_pawn_promotions()
333
+
334
+ return all_moves + pawn_promotions
335
+
336
+
337
+ def chunks(lst, n):
338
+ """Yield successive n-sized chunks from lst."""
339
+ for i in range(0, len(lst), n):
340
+ yield lst[i:i + n]
341
+
342
+
343
+ if __name__ == '__main__':
344
+
345
+ pass
346
+
347
+
@@ -0,0 +1,24 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2024, CSSLab
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.1
2
+ Name: maia2
3
+ Version: 0.1
4
+ Summary: An amazing sample package for maia2.
5
+ Author-email: csslab <josephtang@cs.toronto.edu>
6
+ Description-Content-Type: text/markdown
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Project-URL: Home, https://github.com/CSSLab/maia2
9
+
10
+ # maia2
@@ -0,0 +1,9 @@
1
+ maia2/__init__.py,sha256=8z7obJwpM5l5hGYw8MvEwuViELcuO-D991YkvWhMJh0,64
2
+ maia2/fetch_data.sh,sha256=LefV6tal7pp0a34ODhcx6y1-i0p4m1mLvS1ALIB0PeM,746
3
+ maia2/inference.py,sha256=43rHIrV2oYVLvxYrVF9L_5C4Zad7BjsKGKsVcKPRBuA,5542
4
+ maia2/main.py,sha256=A1YM01iJN2XzGfK1a8vWTB7HbwrjFhgRo2A65qXR6VY,26106
5
+ maia2/utils.py,sha256=dvriII8I6zzS-ZIsNEDDV6tAXfiqaa0OZ0NTOARlgd0,10560
6
+ maia2-0.1.dist-info/LICENSE,sha256=T0nFxRZvtWtfqPnjU6vIWkdL0t13hvAAhwHRZhluelc,1295
7
+ maia2-0.1.dist-info/WHEEL,sha256=Sgu64hAMa6g5FdzHxXv9Xdse9yxpGGMeagVtPMWpJQY,99
8
+ maia2-0.1.dist-info/METADATA,sha256=QWXxHkV-w_G_sm_NgNuFtkyMXsr9hHBm1Yj10JGGf5w,293
9
+ maia2-0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any