Rafs-an09002 commited on
Commit
308bdff
·
verified ·
1 Parent(s): 02b0459

Create engine/evaluate.py

Browse files
Files changed (1) hide show
  1. engine/evaluate.py +236 -0
engine/evaluate.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Position Evaluation Module
3
+ Combines neural network evaluation with classical heuristics
4
+
5
+ Research References:
6
+ - AlphaZero (Silver et al., 2017) - Pure neural evaluation
7
+ - Stockfish NNUE (2020) - Hybrid neural-classical approach
8
+ - Leela Chess Zero - MCTS with neural evaluation
9
+ """
10
+
11
+ import onnxruntime as ort
12
+ import numpy as np
13
+ import chess
14
+ import logging
15
+ from pathlib import Path
16
+ from typing import Dict, Optional
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class NeuralEvaluator:
22
+ """
23
+ Synapse-Base neural network evaluator
24
+ 119-channel input, hybrid CNN-Transformer architecture
25
+ """
26
+
27
+ # Piece values for material balance (Stockfish values)
28
+ PIECE_VALUES = {
29
+ chess.PAWN: 100,
30
+ chess.KNIGHT: 320,
31
+ chess.BISHOP: 330,
32
+ chess.ROOK: 500,
33
+ chess.QUEEN: 900,
34
+ chess.KING: 0
35
+ }
36
+
37
+ # Piece-Square Tables (simplified Stockfish PST)
38
+ PST_PAWN = np.array([
39
+ [0, 0, 0, 0, 0, 0, 0, 0],
40
+ [50, 50, 50, 50, 50, 50, 50, 50],
41
+ [10, 10, 20, 30, 30, 20, 10, 10],
42
+ [5, 5, 10, 25, 25, 10, 5, 5],
43
+ [0, 0, 0, 20, 20, 0, 0, 0],
44
+ [5, -5,-10, 0, 0,-10, -5, 5],
45
+ [5, 10, 10,-20,-20, 10, 10, 5],
46
+ [0, 0, 0, 0, 0, 0, 0, 0]
47
+ ], dtype=np.float32)
48
+
49
+ PST_KNIGHT = np.array([
50
+ [-50,-40,-30,-30,-30,-30,-40,-50],
51
+ [-40,-20, 0, 0, 0, 0,-20,-40],
52
+ [-30, 0, 10, 15, 15, 10, 0,-30],
53
+ [-30, 5, 15, 20, 20, 15, 5,-30],
54
+ [-30, 0, 15, 20, 20, 15, 0,-30],
55
+ [-30, 5, 10, 15, 15, 10, 5,-30],
56
+ [-40,-20, 0, 5, 5, 0,-20,-40],
57
+ [-50,-40,-30,-30,-30,-30,-40,-50]
58
+ ], dtype=np.float32)
59
+
60
+ PST_KING_MG = np.array([
61
+ [-30,-40,-40,-50,-50,-40,-40,-30],
62
+ [-30,-40,-40,-50,-50,-40,-40,-30],
63
+ [-30,-40,-40,-50,-50,-40,-40,-30],
64
+ [-30,-40,-40,-50,-50,-40,-40,-30],
65
+ [-20,-30,-30,-40,-40,-30,-30,-20],
66
+ [-10,-20,-20,-20,-20,-20,-20,-10],
67
+ [ 20, 20, 0, 0, 0, 0, 20, 20],
68
+ [ 20, 30, 10, 0, 0, 10, 30, 20]
69
+ ], dtype=np.float32)
70
+
71
+ def __init__(self, model_path: str, num_threads: int = 2):
72
+ """Initialize neural evaluator"""
73
+
74
+ self.model_path = Path(model_path)
75
+ if not self.model_path.exists():
76
+ raise FileNotFoundError(f"Model not found: {model_path}")
77
+
78
+ # ONNX Runtime session (CPU optimized)
79
+ sess_options = ort.SessionOptions()
80
+ sess_options.intra_op_num_threads = num_threads
81
+ sess_options.inter_op_num_threads = num_threads
82
+ sess_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
83
+ sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
84
+
85
+ logger.info(f"Loading Synapse-Base model from {model_path}...")
86
+ self.session = ort.InferenceSession(
87
+ str(self.model_path),
88
+ sess_options=sess_options,
89
+ providers=['CPUExecutionProvider']
90
+ )
91
+
92
+ self.input_name = self.session.get_inputs()[0].name
93
+ self.output_names = [output.name for output in self.session.get_outputs()]
94
+
95
+ logger.info(f"✅ Model loaded: {self.input_name} -> {self.output_names}")
96
+
97
+ def _build_119_channel_tensor(self, board: chess.Board) -> np.ndarray:
98
+ """
99
+ Convert board to 119-channel tensor
100
+ Based on Synapse-Base input specification
101
+ """
102
+ tensor = np.zeros((1, 119, 8, 8), dtype=np.float32)
103
+
104
+ # === CHANNELS 0-11: Piece Positions ===
105
+ piece_map = board.piece_map()
106
+ piece_to_channel = {
107
+ chess.PAWN: 0, chess.KNIGHT: 1, chess.BISHOP: 2,
108
+ chess.ROOK: 3, chess.QUEEN: 4, chess.KING: 5
109
+ }
110
+
111
+ for square, piece in piece_map.items():
112
+ rank, file = divmod(square, 8)
113
+ channel = piece_to_channel[piece.piece_type]
114
+ if piece.color == chess.BLACK:
115
+ channel += 6
116
+ tensor[0, channel, rank, file] = 1.0
117
+
118
+ # === CHANNELS 12-26: Metadata ===
119
+ tensor[0, 12, :, :] = float(board.turn == chess.WHITE)
120
+ tensor[0, 13, :, :] = float(board.has_kingside_castling_rights(chess.WHITE))
121
+ tensor[0, 14, :, :] = float(board.has_queenside_castling_rights(chess.WHITE))
122
+ tensor[0, 15, :, :] = float(board.has_kingside_castling_rights(chess.BLACK))
123
+ tensor[0, 16, :, :] = float(board.has_queenside_castling_rights(chess.BLACK))
124
+
125
+ if board.ep_square is not None:
126
+ ep_rank, ep_file = divmod(board.ep_square, 8)
127
+ tensor[0, 17, ep_rank, ep_file] = 1.0
128
+
129
+ tensor[0, 18, :, :] = min(board.halfmove_clock / 100.0, 1.0)
130
+ tensor[0, 19, :, :] = min(board.fullmove_number / 100.0, 1.0)
131
+ tensor[0, 20, :, :] = float(board.is_check() and board.turn == chess.WHITE)
132
+ tensor[0, 21, :, :] = float(board.is_check() and board.turn == chess.BLACK)
133
+
134
+ # Material counts
135
+ for i, piece_type in enumerate([chess.PAWN, chess.KNIGHT, chess.BISHOP, chess.ROOK, chess.QUEEN]):
136
+ white_count = len(board.pieces(piece_type, chess.WHITE))
137
+ black_count = len(board.pieces(piece_type, chess.BLACK))
138
+ max_count = 8 if piece_type == chess.PAWN else 2
139
+ tensor[0, 22 + i*2, :, :] = white_count / max_count
140
+ tensor[0, 23 + i*2, :, :] = black_count / max_count
141
+
142
+ # === CHANNELS 27-50: Attack Maps ===
143
+ for square in chess.SQUARES:
144
+ rank, file = divmod(square, 8)
145
+ if board.is_attacked_by(chess.WHITE, square):
146
+ tensor[0, 27, rank, file] = 1.0
147
+ if board.is_attacked_by(chess.BLACK, square):
148
+ tensor[0, 28, rank, file] = 1.0
149
+
150
+ # Mobility (number of legal moves)
151
+ white_mobility = len(list(board.legal_moves)) if board.turn == chess.WHITE else 0
152
+ black_mobility = len(list(board.legal_moves)) if board.turn == chess.BLACK else 0
153
+ tensor[0, 29, :, :] = min(white_mobility / 50.0, 1.0)
154
+ tensor[0, 30, :, :] = min(black_mobility / 50.0, 1.0)
155
+
156
+ # === CHANNELS 51-66: Coordinate Encoding ===
157
+ for rank in range(8):
158
+ tensor[0, 51 + rank, rank, :] = 1.0
159
+ for file in range(8):
160
+ tensor[0, 59 + file, :, file] = 1.0
161
+
162
+ # === CHANNELS 67-118: Positional Features ===
163
+ # Center control
164
+ center = [chess.D4, chess.D5, chess.E4, chess.E5]
165
+ for sq in center:
166
+ r, f = divmod(sq, 8)
167
+ tensor[0, 67, r, f] = 0.5
168
+
169
+ # King safety zones
170
+ for color, offset in [(chess.WHITE, 68), (chess.BLACK, 69)]:
171
+ king_sq = board.king(color)
172
+ if king_sq is not None:
173
+ kr, kf = divmod(king_sq, 8)
174
+ for dr in [-1, 0, 1]:
175
+ for df in [-1, 0, 1]:
176
+ r, f = kr + dr, kf + df
177
+ if 0 <= r < 8 and 0 <= f < 8:
178
+ tensor[0, offset, r, f] = 1.0
179
+
180
+ # Piece-square table values
181
+ for square, piece in piece_map.items():
182
+ rank, file = divmod(square, 8)
183
+ if piece.piece_type == chess.PAWN:
184
+ pst_value = self.PST_PAWN[rank, file] / 50.0
185
+ tensor[0, 70, rank, file] = pst_value if piece.color == chess.WHITE else -pst_value
186
+ elif piece.piece_type == chess.KNIGHT:
187
+ pst_value = self.PST_KNIGHT[rank, file] / 30.0
188
+ tensor[0, 71, rank, file] = pst_value if piece.color == chess.WHITE else -pst_value
189
+
190
+ return tensor
191
+
192
+ def evaluate_neural(self, board: chess.Board) -> float:
193
+ """
194
+ Neural network evaluation
195
+ Returns score from white's perspective
196
+ """
197
+ input_tensor = self._build_119_channel_tensor(board)
198
+ outputs = self.session.run(self.output_names, {self.input_name: input_tensor})
199
+
200
+ # Value head output (tanh normalized to [-1, 1])
201
+ raw_eval = float(outputs[0][0][0])
202
+
203
+ # Convert to centipawns (multiply by 4 for scaling)
204
+ return raw_eval * 400.0
205
+
206
+ def evaluate_material(self, board: chess.Board) -> int:
207
+ """Classical material evaluation"""
208
+ material = 0
209
+ for piece_type in [chess.PAWN, chess.KNIGHT, chess.BISHOP, chess.ROOK, chess.QUEEN]:
210
+ material += len(board.pieces(piece_type, chess.WHITE)) * self.PIECE_VALUES[piece_type]
211
+ material -= len(board.pieces(piece_type, chess.BLACK)) * self.PIECE_VALUES[piece_type]
212
+ return material
213
+
214
+ def evaluate_hybrid(self, board: chess.Board) -> float:
215
+ """
216
+ Hybrid evaluation combining neural and classical
217
+ Research: Stockfish NNUE approach
218
+ """
219
+ # Neural evaluation (primary)
220
+ neural_eval = self.evaluate_neural(board)
221
+
222
+ # Classical material balance (safety check)
223
+ material_eval = self.evaluate_material(board)
224
+
225
+ # Blend: 95% neural, 5% material (for stability)
226
+ hybrid_eval = 0.95 * neural_eval + 0.05 * material_eval
227
+
228
+ # Perspective flip for black
229
+ if board.turn == chess.BLACK:
230
+ hybrid_eval = -hybrid_eval
231
+
232
+ return hybrid_eval
233
+
234
+ def get_model_size_mb(self) -> float:
235
+ """Get model size in MB"""
236
+ return self.model_path.stat().st_size / (1024 * 1024)